TextKit介绍(转载3篇文章)

来源:互联网 发布:淘宝网logo图片 编辑:程序博客网 时间:2024/06/05 14:44
TextKit
以前,如果我们想实现如上图所示复杂的文本排版:显示不同样式的文本、图片和文字混排,你可能就需要借助于UIWebView或者深入研究一下Core Text。在iOS6中,UILabel、UITextField、UITextView增加了一个NSAttributedString属性,可以稍微解决一些排版问题,但是支持的力度还不够。现在Text Kit完全改变了这种现状。

1.NSAttributedString

下面的例子,展示如何label中显示属性化字符串:

-(void)setAttributeStringLabel{    NSString *str = @"bold,little color,hello";    //NSMutableAttributedString的初始化    NSDictionary *attrs = @{NSFontAttributeName:[UIFont preferredFontForTextStyle:UIFontTextStyleBody]};    NSMutableAttributedString *attributedString = [[NSMutableAttributedString alloc]initWithString:str attributes:attrs];    //NSMutableAttributedString增加属性    [attributedString addAttribute:NSFontAttributeName value:[UIFont boldSystemFontOfSize:36] range:[str rangeOfString:@"bold"]];    [attributedString addAttribute:NSForegroundColorAttributeName value:[UIColor blueColor] range:[str rangeOfString:@"little color"]];    [attributedString addAttribute:NSFontAttributeName value:[UIFont fontWithName:@"Papyrus" size:36] range:NSMakeRange(18,5)];    [attributedString addAttribute:NSFontAttributeName value:[UIFont boldSystemFontOfSize:18] range:[str rangeOfString:@"little"]];    //NSMutableAttributedString移除属性    [attributedString removeAttribute:NSFontAttributeName range:[str rangeOfString:@"little"]];    //NSMutableAttributedString设置属性    NSDictionary *attrs2 = @{NSStrokeWidthAttributeName:@-5,                             NSStrokeColorAttributeName:[UIColor greenColor],                             NSFontAttributeName:[UIFont systemFontOfSize:36],                             NSUnderlineStyleAttributeName:@(NSUnderlineStyleSingle)};    [attributedString setAttributes:attrs2 range:NSMakeRange(0, 4)];    self.label.attributedText = attributedString;}

运行结果如下:


需要注意的是,你不能直接修改已有的AttributedString, 你需要把它copy出来,修改后再进行设置:

NSMutableAttributedString *labelText = [myLabel.attributedText mutableCopy]; [labelText setAttributes:...];myLabel.attributedText = labelText;

2.Dynamic type:动态字体

iOS7增加了一项用户偏好设置:动态字体,用户可以通过显示与亮度-文字大小设置面板来修改设备上所有字体的尺寸。为了支持这个特性,意味着不要用systemFontWithSize:,而要用新的字体选择器preferredFontForTextStyle:。iOS提供了六种样式:标题,正文,副标题,脚注,标题1,标题2。例如:

_textView.font = [UIFont preferredFontForTextStyle:UIFontTextStyleBody];

你可以接收用户改变字体大小的通知:

[[NSNotificationCenter defaultCenter] addObserver:self                                             selector:@selector(preferredContentSizeChanged:) name:UIContentSizeCategoryDidChangeNotification                                               object:nil];-(void)preferredContentSizeChanged:(NSNotification *)notification{    _textView.font = [UIFont preferredFontForTextStyle:UIFontTextStyleBody];}

3.Exclusion paths:排除路径

iOS 上的 NSTextContainer 提供了exclusionPaths,它允许开发者设置一个 NSBezierPath 数组来指定不可填充文本的区域。如下图:

IMG_0934.PNG
IMG_0934.PNG

正如你所看到的,所有的文本都放置在蓝色椭圆外面。在 Text View 里面实现这个行为很简单,但是有个小麻烦:Bezier Path 的坐标必须使用容器的坐标系。以下是转换方法,将它的 bounds(self.circleView.bounds)转换到 Text View 的坐标系统:

- (void)updateExclusionPaths{    CGRect ovalFrame = [self.textView convertRect:self.circleView.bounds fromView:self.circleView];    }

因为没有 inset,文本会过于靠近视图边界,所以 UITextView 会在离边界还有几个点的距离的地方插入它的文本容器。因此,要得到以容器坐标表示的路径,必须从 origin 中减去这个插入点的坐标。

ovalFrame.origin.x -= self.textView.textContainerInset.left;ovalFrame.origin.y -= self.textView.textContainerInset.top;

在此之后,只需将 Bezier Path 设置给 Text Container 即可将对应的区域排除掉。其它的过程对你来说是透明的,TextKit 会自动处理。

self.textView.textContainer.exclusionPaths = @[[UIBezierPath bezierPathWithOvalInRect: ovalFrame]];

4.多容器布局

屏幕快照 2015-03-10 下午2.52.15.png
屏幕快照 2015-03-10 下午2.52.15.png

NSTextStorage:它是NSMutableAttributedString的子类,里面存的是要管理的文本。
NSLayoutManager:管理文本布局方式
NSTextContainer:表示文本要填充的区域

如上图所示,它们的关系是 1 对 N 的关系。就是那样:一个 Text Storage 可以拥有多个 Layout Manager,一个 Layout Manager 也可以拥有多个 Text Container。这些多重性带来了多容器布局的特性:

1)将多个 Layout Manager 附加到同一个 Text Storage 上,可以产生相同文本的多种视觉表现,如果相应的 Text View 可编辑,那么在某个 Text View 上做的所有修改都会马上反映到所有 Text View 上。

    NSTextStorage *sharedTextStorage = self.originalTextView.textStorage;    [sharedTextStorage replaceCharactersInRange:NSMakeRange(0, 0) withString:kstring];    // 将一个新的 Layout Manager 附加到上面的 Text Storage 上    NSLayoutManager *otherLayoutManager = [NSLayoutManager new];    [sharedTextStorage addLayoutManager: otherLayoutManager];    NSTextContainer *otherTextContainer = [NSTextContainer new];    [otherLayoutManager addTextContainer: otherTextContainer];    UITextView *otherTextView = [[UITextView alloc] initWithFrame:self.otherContainerView.bounds textContainer:otherTextContainer];    otherTextView.backgroundColor = self.otherContainerView.backgroundColor;    otherTextView.translatesAutoresizingMaskIntoConstraints = YES;    otherTextView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;    otherTextView.scrollEnabled = NO;    [self.otherContainerView addSubview: otherTextView];    self.otherTextView = otherTextView;

2)将多个 Text Container 附加到同一个 Layout Manager 上,这样可以将一个文本分布到多个视图展现出来。下面的例子将展示这两个特性:

// 将一个新的 Text Container 附加到同一个 Layout Manager,这样可以将一个文本分布到多个视图展现出来。    NSTextContainer *thirdTextContainer = [NSTextContainer new];    [otherLayoutManager addTextContainer: thirdTextContainer];    UITextView *thirdTextView = [[UITextView alloc] initWithFrame:self.thirdContainerView.bounds textContainer:thirdTextContainer];    thirdTextView.backgroundColor = self.thirdContainerView.backgroundColor;    thirdTextView.translatesAutoresizingMaskIntoConstraints = YES;    thirdTextView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;    [self.thirdContainerView addSubview: thirdTextView];    self.thirdTextView = thirdTextView;

结果如下所示:

IMG_0935.PNG
IMG_0935.PNG

5.语法高亮:继承NSTextStorage

看看 TextKit 组件的责任划分,就很清楚语法高亮应该由 Text Storage 实现。不过NSTextStorage 不是一个普通的类,它是一个类簇,你可以把它理解为一个"半具体"子类,因此要继承它必须实现以下方法:

- string;- attributesAtIndex:effectiveRange:- replaceCharactersInRange:withString:- setAttributes:range:

我们新建一个NSTextStorage的子类:SyntaxHighlightTextStorage

要实现以上4个方法,我们首先需要通过NSMutableAttributedString 实现一个后备存储,- setAttributes:range:这个方法需要用beginEditing和endEditing包起来,而且必须调用 edited:range:changeInLength:,所以大部分的NSTextStorage的子类都长下面这个样子:

@implementation SyntaxHighlightTextStorage{    NSMutableAttributedString *_backingStore;}- (instancetype)init{    self = [super init];    if (self) {        _backingStore = [NSMutableAttributedString new];    }    return self;}//1- (NSString *)string {    return [_backingStore string];}//2- (NSDictionary *)attributesAtIndex:(NSUInteger)location effectiveRange:(NSRangePointer)range{    return [_backingStore attributesAtIndex:location                             effectiveRange:range];}//3- (void)replaceCharactersInRange:(NSRange)range withString:(NSString *)str{    NSLog(@"replaceCharactersInRange:%@ withString:%@",NSStringFromRange(range), str);    [self beginEditing];    [_backingStore replaceCharactersInRange:range withString:str];    [self edited:NSTextStorageEditedCharacters | NSTextStorageEditedAttributes range:range changeInLength:str.length - range.length];    [self endEditing];}//4- (void)setAttributes:(NSDictionary *)attrs range:(NSRange)range {    NSLog(@"setAttributes:%@ range:%@", attrs, NSStringFromRange(range));    [self beginEditing];    [_backingStore setAttributes:attrs range:range];    [self edited:NSTextStorageEditedAttributes range:range changeInLength:0];    [self endEditing];}

一个方便实现高亮的办法是覆盖 -processEditing,并设置一个正则表达式来查找单词,每次文本存储有修改时,这个方法都自动被调用。

- (void)processEditing{    [super processEditing];    static NSRegularExpression *expression;    expression = expression ?: [NSRegularExpression regularExpressionWithPattern:@"(\\*\\w+(\\s\\w+)*\\*)\\s" options:0 error:NULL];   }

首先清除之前所有的高亮:

NSRange paragaphRange = [self.string paragraphRangeForRange: self.editedRange];    [self removeAttribute:NSForegroundColorAttributeName range:paragaphRange];

其次遍历所有的样式匹配项并高亮它们:

[expression enumerateMatchesInString:self.string options:0 range:paragaphRange usingBlock:^(NSTextCheckingResult *result, NSMatchingFlags flags, BOOL *stop) {        [self addAttribute:NSForegroundColorAttributeName value:[UIColor blueColor] range:result.range];    }];

就这样,我们在文本系统栈里面有了一个 Text Storage 的全功能替换版本。在从 Interface 文件中载入时,可以像这样将它插入文本视图:

- (void)createTextView {    _textStorage = [SyntaxHighlightTextStorage new];    [_textStorage addLayoutManager: self.textView.layoutManager];    [_textStorage replaceCharactersInRange:NSMakeRange(0, 0) withString:@"在从 Interface 文件中载入时,可以像这样将它插入文本视图,然后加 *星号* 的字就会高亮出来了"];    _textView.delegate = self;}

运行如下:

IMG_0936.PNG
IMG_0936.PNG

6.文本容器修改:继承NSTextContainer

通过继承NSTextContainer,我们可以使得textView不再是一个规规矩矩的矩形。NSTextContainer负责回答这个问题:对于给定的矩形,哪个部分可以放文字,这个问题由下面这个方法来回答:

- (CGRect)lineFragmentRectForProposedRect: atIndex: writingDirection: remainingRect:

所以我们在继承NSTextContainer的类中覆盖这个方法即可:

下面这个方法返回一个圆形区域:

- (CGRect)lineFragmentRectForProposedRect:(CGRect)proposedRect                                  atIndex:(NSUInteger)characterIndex                         writingDirection:(NSWritingDirection)baseWritingDirection                            remainingRect:(CGRect *)remainingRect {  CGRect rect = [super lineFragmentRectForProposedRect:proposedRect                                               atIndex:characterIndex                                      writingDirection:baseWritingDirection                                         remainingRect:remainingRect];  CGSize size = [self size];  CGFloat radius = fmin(size.width, size.height) / 2.0;  CGFloat ypos = fabs((proposedRect.origin.y + proposedRect.size.height / 2.0) - radius);  CGFloat width = (ypos < radius) ? 2.0 * sqrt(radius * radius - ypos * ypos) : 0.0;  CGRect circleRect = CGRectMake(radius - width / 2.0, proposedRect.origin.y, width, proposedRect.size.height);  return CGRectIntersection(rect, circleRect);}

使用这个继承类:

- (void)viewDidLoad {    [super viewDidLoad];    NSString *path = [[NSBundle mainBundle] pathForResource:@"sample.txt" ofType:nil];    NSString *string = [NSString stringWithContentsOfFile:path encoding:NSUTF8StringEncoding error:nil];    NSMutableParagraphStyle *style = [NSMutableParagraphStyle new];    [style setAlignment:NSTextAlignmentJustified];    NSTextStorage *text = [[NSTextStorage alloc] initWithString:string                                                     attributes:@{                                                                  NSParagraphStyleAttributeName: style,                                                                  NSFontAttributeName: [UIFont preferredFontForTextStyle:UIFontTextStyleCaption2]                                                                  }];    NSLayoutManager *layoutManager = [NSLayoutManager new];    [text addLayoutManager:layoutManager];    CGRect textViewFrame = CGRectMake(20, 20, 280, 280);    CircleTextContainer *textContainer = [[CircleTextContainer alloc] initWithSize:textViewFrame.size];    [textContainer setExclusionPaths:@[ [UIBezierPath bezierPathWithOvalInRect:CGRectMake(80, 120, 50, 50)]]];    [layoutManager addTextContainer:textContainer];    UITextView *textView = [[UITextView alloc] initWithFrame:textViewFrame                                               textContainer:textContainer];    textView.allowsEditingTextAttributes = YES;    textView.scrollEnabled = NO;    textView.editable = NO;    [self.view addSubview:textView];}

效果如下:

IMG_0937.PNG
IMG_0937.PNG

7.布局修改:继承NSLayoutManager

利用NSLayoutManager的代理方法,我们可以轻松的设置行高:

- (CGFloat)layoutManager:(NSLayoutManager *)layoutManager  lineSpacingAfterGlyphAtIndex:(NSUInteger)glyphIndex  withProposedLineFragmentRect:(CGRect)rect{    return floorf(glyphIndex / 100);}

假设你的文本中有链接,你不希望这些链接被断行分割。如果可能的话,一个 URL 应该始终显示为一个整体,一个单一的文本片段。没有什么比这更简单的了。

首先,就像前面讨论过的那样,我们使用自定义的 Text Storage,如下:

static NSDataDetector *linkDetector;linkDetector = linkDetector ?: [[NSDataDetector alloc] initWithTypes:NSTextCheckingTypeLink error:NULL];NSRange paragaphRange = [self.string paragraphRangeForRange: NSMakeRange(range.location, str.length)];[self removeAttribute:NSLinkAttributeName range:paragaphRange];[linkDetector enumerateMatchesInString:self.string                               options:0                                 range:paragaphRange                            usingBlock:^(NSTextCheckingResult *result, NSMatchingFlags flags, BOOL *stop){    [self addAttribute:NSLinkAttributeName value:result.URL range:result.range];}];

改变断行行为就只需要实现一个 Layout Manager 的代理方法:

- (BOOL)layoutManager:(NSLayoutManager *)layoutManager shouldBreakLineByWordBeforeCharacterAtIndex:(NSUInteger)charIndex{    NSRange range;    NSURL *linkURL = [layoutManager.textStorage attribute:NSLinkAttributeName                                                  atIndex:charIndex                                           effectiveRange:&range];    return !(linkURL && charIndex > range.location && charIndex <= NSMaxRange(range));

结果就像下面这样:

IMG_0938.PNG
IMG_0938.PNG

你可以在这里下载到本文的代码。
参考:初识 TextKit,iOS 7 by Tutorials,iOS 7 Programming Pushing the Limits
文/庄洁元(简书作者)
原文链接:http://www.jianshu.com/p/2f72a5fa99f1
著作权归作者所有,转载请联系作者获得授权,并标注“简书作者”。






Text Kit学习(入门和进阶)

转自TracyYih的博客

 

更详细的内容可以参考官方文档 《Text Programming Guide for iOS》。

 

“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的子类,支持分批编辑。这就意味着你可以改变一个范围内的字符的样式而不用整体替换文本内容。
 
  1. [self.textView.textStorage beginEditing]; 
  2. [self markWord:@"Alice" inTextStorage:self.textView.textStorage]; 
  3. [self.textView.textStorage endEditing]; 
 
Text storage管理者一系列的NSLayoutManager对象,当它的字符或者属性改变时会通知到自己所管理的layout Manager对象以便它们作出相应的反应。在layout manager上面是一个NSTextContainer对象,用于为layout manager定义坐标系和一些几何特性。例如,如果你想UITextView中的文本环绕在一张图片四周,你可以给text container设定一个排除路径(exclusion path)。
 
  1. UIBezierPath *exclusion = ButterflyBezierPath; 
  2. self.textView.textContainer.exclusionPaths = @[exclusion]; 
 
 
Text container能够处理击中测试(hit tests),所以可以定位到点击的字符在文本中的位置。此外它还提供一些代理方法让开发者能够自己定义链接点击后的处理事件。
 
 
通过基于Text Kit重新构建UILabel、UITextField和UITextView,苹果给开发者更大的灵活性和能力来设计富文本视图,同时简化了这些控件的使用,因为它们是以同样的方式设计的,所有这些好处都是站在巨人(Core Text)的肩上。通常更强大的功能和灵活性也就意味着需要更多的设置和管理,但是,如果你只是想显示一段简单的文本,你还是可以像以前一样使用。
 
  1. self.textLabel.text = @"Hello Text Kit"
 
 本文翻译自《iOS 7: 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)设定字体:
 
  1. self.textView.font = [UIFont preferredFontForTextStyle:UIFontTextStyleBody]; 
 
这样设置之后,文本控件就会以用户设定的字体大小及粗细显示,但是如果程序在运行时,用户切换到设置里修改了字体,这是在切回程序,字体并不会自动跟着变。这时就需要我们自己来更新一下控件的字体了。
 
在系统字体修改时,系统会给运行中的程序发送UIContentSizeCategoryDidChangeNotification通知,我们只需要监听这个通知,并重新设置一下字体即可。
 
  1. [[NSNotificationCenter defaultCenter] addObserver:self 
  2.     selector:@selector(preferredContentSizeChanged:) 
  3.     name:UIContentSizeCategoryDidChangeNotification 
  4.     object:nil]; 
 
 
  1. - (void)preferredContentSizeChanged:(NSNotification *)notification{ 
  2.     self.textView.font = [UIFont preferredFontForTextStyle:UIFontTextStyleBody]; 
当然,有的时候要适应动态修改的字体并不是这么设置一下就完事了,控件的大小可能也需要进行相应的调整,这时我们程序中的控件大小也不应该写死,而是需要根据字体大小来计算.
 
凸版印刷体效果(Letterpress effects)
凸版印刷替效果是给文字加上奇妙阴影和高光,让文字看起有凹凸感,像是被压在屏幕上。当然这种看起来很高端大气上档次的效果实现起来确实相当的简单,只需要给AttributedString加一个NSTextEffectAttributeName属性,并指定该属性值为NSTextEffectLetterpressStyle就可以了。
 
  1. tionary *attributes = @{  
  2.     NSForegroundColorAttributeName: [UIColor redColor], 
  3.     NSFontAttributeName: [UIFont preferredFontForTextStyle:UIFontTextStyleHeadline], 
  4.     NSTextEffectAttributeName: NSTextEffectLetterpressStyle 
  5. }; 
  6. self.titleLabel.attributedText = [[NSAttributedString alloc] initWithString:@"Title" attributes:attributes]; 
 
 
在iOS7系统自带的备忘录应用中,苹果就使用了这种凸版印刷体效果。
 
路径排除(Exclusion paths)
在排版中,图文混排是非常常见的需求,但有时候我们的图片并一定都是正常的矩形,这个时候我们如果需要将文本环绕在图片周围,就可以用路径排除(exclusion paths)了。
 
Explosion pats基本原理是将需要被文本留出来的形状的路径告诉文本控件的NSTextContainer对象,NSTextContainer在文字排版时就会避开该路径。
 
  1. UIBezierPath *floatingPath = [self pathOfImage]; 
  2. 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,在实现文件中添加一个成员变量:
 
  1. #import "MarkupTextStorage.h" 
  2.  
  3. @implementation MarkupTextStorage 
  4.     NSMutableAttributedString *_backingStore; 
  5.  
  6. - (id)init 
  7.     self = [super init]; 
  8.     if (self) { 
  9.         _backingStore = [[NSMutableAttributedString alloc] init]; 
  10.     } 
  11.     return self; 
  12.  
  13. @end 
 
NSTextStorage的子类需要重载一些方法提供NSMutableAttributedString类型的backing store信息,所以我们继续添加下面代码:
 
  1. - (NSString *)string 
  2.     return [_backingStore string]; 
  3.  
  4. - (NSDictionary *)attributesAtIndex:(NSUInteger)location effectiveRange:(NSRangePointer)range 
  5.     return [_backingStore attributesAtIndex:location effectiveRange:range]; 
  6.  
  7. - (void)replaceCharactersInRange:(NSRange)range withString:(NSString *)str 
  8.     [self beginEditing]; 
  9.     [_backingStore replaceCharactersInRange:range withString:str]; 
  10.     [self edited:NSTextStorageEditedCharacters | NSTextStorageEditedAttributes 
  11.            range:range changeInLength:str.length - range.length]; 
  12.     [self endEditing]; 
  13.  
  14. - (void)setAttributes:(NSDictionary *)attrs range:(NSRange)range 
  15.     [self beginEditing]; 
  16.     [_backingStore setAttributes:attrs range:range]; 
  17.     [self edited:NSTextStorageEditedAttributes 
  18.            range:range changeInLength:0]; 
  19.     [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
  1. - (void)createMarkupTextView 
  2.     NSDictionary *attributes = @{NSFontAttributeName: [UIFont preferredFontForTextStyle:UIFontTextStyleBody]}; 
  3.     NSString *content = [NSString stringWithContentsOfFile:[[NSBundle mainBundle] pathForResource:@"content" ofType:@"txt"
  4.                                                   encoding:NSUTF8StringEncoding 
  5.                                                      error:nil]; 
  6.     NSAttributedString *attributedString = [[NSAttributedString alloc] initWithString:content 
  7.                                                                            attributes:attributes]; 
  8.     _textStorage = [[MarkupTextStorage alloc] init]; 
  9.     [_textStorage setAttributedString:attributedString]; 
  10.      
  11.     CGRect textViewRect = CGRectMake(20, 60, 280, self.view.bounds.size.height - 100); 
  12.      
  13.     NSLayoutManager *layoutManager = [[NSLayoutManager alloc] init]; 
  14.      
  15.     NSTextContainer *textContainer = [[NSTextContainer alloc] initWithSize:CGSizeMake(textViewRect.size.width, CGFLOAT_MAX)]; 
  16.     [layoutManager addTextContainer:textContainer]; 
  17.     [_textStorage addLayoutManager:layoutManager]; 
  18.      
  19.     _textView = [[UITextView alloc] initWithFrame:textViewRect 
  20.                                     textContainer:textContainer]; 
  21.     _textView.delegate = self; 
  22.     [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文件中添加如下方法:
  1. - (void)processEditing 
  2.     [self performReplacementsForRange:[self editedRange]]; 
  3.     [super processEditing]; 
 
 processEditing在layout manager中文本修改时发送通知,它通常也是处理一些文本修改逻辑的好地方。
 
继续添加:
  1. - (void)performReplacementsForRange:(NSRange)changedRange 
  2.     NSRange extendedRange = NSUnionRange(changedRange, [[_backingStore string] 
  3.                                                         lineRangeForRange:NSMakeRange(changedRange.location, 0)]); 
  4.     extendedRange = NSUnionRange(changedRange, [[_backingStore string] 
  5.                                                 lineRangeForRange:NSMakeRange(NSMaxRange(changedRange), 0)]); 
  6.     [self applyStylesToRange:extendedRange]; 
 
这个方法用于扩大文本匹配的范围,因为changedRange只是标识出一个字符,lineRangeForRange会将范围扩大到当前的一整行。
 
下面就剩下匹配特定格式的文本来显示对应的样式了:
  1. - (NSDictionary*)createAttributesForFontStyle:(NSString*)style 
  2.                                     withTrait:(uint32_t)trait { 
  3.     UIFontDescriptor *fontDescriptor = [UIFontDescriptor 
  4.                                         preferredFontDescriptorWithTextStyle:UIFontTextStyleBody]; 
  5.      
  6.     UIFontDescriptor *descriptorWithTrait = [fontDescriptor 
  7.                                              fontDescriptorWithSymbolicTraits:trait]; 
  8.      
  9.     UIFont* font =  [UIFont fontWithDescriptor:descriptorWithTrait size: 0.0]; 
  10.     return @{ NSFontAttributeName : font }; 
  11.  
  12. - (void)createMarkupStyledPatterns  
  13.     UIFontDescriptor *scriptFontDescriptor = 
  14.     [UIFontDescriptor fontDescriptorWithFontAttributes: 
  15.      @{UIFontDescriptorFamilyAttribute: @"Bradley Hand"}]; 
  16.      
  17.     // 1. base our script font on the preferred body font size 
  18.     UIFontDescriptor* bodyFontDescriptor = [UIFontDescriptor 
  19.                                             preferredFontDescriptorWithTextStyle:UIFontTextStyleBody]; 
  20.     NSNumber* bodyFontSize = bodyFontDescriptor. 
  21.     fontAttributes[UIFontDescriptorSizeAttribute]; 
  22.     UIFont* scriptFont = [UIFont 
  23.                           fontWithDescriptor:scriptFontDescriptor size:[bodyFontSize floatValue]]; 
  24.      
  25.     // 2. create the attributes 
  26.     NSDictionary* boldAttributes = [self 
  27.                                     createAttributesForFontStyle:UIFontTextStyleBody 
  28.                                     withTrait:UIFontDescriptorTraitBold]; 
  29.     NSDictionary* italicAttributes = [self 
  30.                                       createAttributesForFontStyle:UIFontTextStyleBody 
  31.                                       withTrait:UIFontDescriptorTraitItalic]; 
  32.     NSDictionary* strikeThroughAttributes = @{ NSStrikethroughStyleAttributeName : @1, 
  33.                                                NSForegroundColorAttributeName: [UIColor redColor]}; 
  34.     NSDictionary* scriptAttributes = @{ NSFontAttributeName : scriptFont, 
  35.                                         NSForegroundColorAttributeName: [UIColor blueColor] 
  36.                                         }; 
  37.     NSDictionary* redTextAttributes = 
  38.     @{ NSForegroundColorAttributeName : [UIColor redColor]}; 
  39.      
  40.     _replacements = @{ 
  41.                       @"(\\*\\*\\w+(\\s\\w+)*\\*\\*)" : boldAttributes, 
  42.                       @"(_\\w+(\\s\\w+)*_)" : italicAttributes, 
  43.                       @"(~~\\w+(\\s\\w+)*~~)" : strikeThroughAttributes, 
  44.                       @"(`\\w+(\\s\\w+)*`)" : scriptAttributes, 
  45.                       @"\\s([A-Z]{2,})\\s" : redTextAttributes 
  46.                       }; 
  47.  
  48. - (void)applyStylesToRange:(NSRange)searchRange 
  49.     NSDictionary* normalAttrs = @{NSFontAttributeName: 
  50.                                       [UIFont preferredFontForTextStyle:UIFontTextStyleBody]}; 
  51.      
  52.     // iterate over each replacement 
  53.     for (NSString* key in _replacements) { 
  54.         NSRegularExpression *regex = [NSRegularExpression 
  55.                                       regularExpressionWithPattern:key 
  56.                                       options:0 
  57.                                       error:nil]; 
  58.          
  59.         NSDictionary* attributes = _replacements[key]; 
  60.          
  61.         [regex enumerateMatchesInString:[_backingStore string] 
  62.                                 options:0 
  63.                                   range:searchRange 
  64.                              usingBlock:^(NSTextCheckingResult *match, 
  65.                                           NSMatchingFlags flags, 
  66.                                           BOOL *stop){ 
  67.                                  // apply the style 
  68.                                  NSRange matchRange = [match rangeAtIndex:1]; 
  69.                                  [self addAttributes:attributes range:matchRange]; 
  70.                                   
  71.                                  // reset the style to the original 
  72.                                  if (NSMaxRange(matchRange)+1 < self.length) { 
  73.                                      [self addAttributes:normalAttrs 
  74.                                                    range:NSMakeRange(NSMaxRange(matchRange)+1, 1)]; 
  75.                                  } 
  76.                              }]; 
  77.     } 
 在createMarkupStyledPatterns初始化方法中调用createMarkupStyledPatterns,通过正则表达式来给特定格式的字符串设定特定显示样式,形成一个对应的字典。然后在applyStylesToRange:中利用已定义好的样式字典来给匹配的文本端增加样式。
 
到这里本篇文章的内容就结束了,其实前面三点都很简单,稍微过一下就能用。最后一个动态文本格式化内容稍微多一点,可以结合我的代码TextKitDemo来看。
 
参考链接:
http://www.raywenderlich.com/50151/text-kit-tutorial
https://developer.apple.com/library/ios/documentation/StringsTextFonts/Conceptual/TextAndWebiPhoneOS/Introduction/Introduction.html
http://adcdownload.apple.com/wwdc_2013/wwdc_2013_sample_code/ios_intrototextkit.zip
iOS 7系列译文:认识 TextKit
iOS7 的发布给开发者的案头带来了很多新工具。其中一个就是 TextKit(文本工具箱)。TextKit 由许多新的 UIKit 类组成,顾名思义,这些类就是用来处理文本的。在这里,我们将介绍 TextKit 的来由、它的组成,以及通过几个例子解释开发者怎样将它派上大用场。
 
但是首先我们得有一点背景知识:TextKit 可能是近期对 UIKit 最重要的补充了。iOS7 的新界面用纯文本按钮替换了大量的图标和边框。总的来说,文本和文本布局在新的操作系统的外观方面比以前重要多了。iOS7 的重新设计完全是被文本驱动,这样说也许并不夸张——而文本全部是TextKit来处理的。
 
告诉你这个变动到底有多大吧:iOS7 之前的所有版本,(几乎)所有的文本都是 WebKit 来处理的。对:WebKit,web 浏览器引擎。所有UILabel、UITextField,以及 UITextView 都在后台以某种方式使用 web 视图来进行文本布局和渲染。为了新的界面风格,它们全都被重新设计以使用TextKit。
 
iOS上文本的简短历史
 
这些新类并不是用来替换开发者以前使用的类。对 SDK 来说,TextKit 提供的是全新的功能。iOS7 之前,TextKit 提供的功能必须都手动完成。这是现有功能之间缺失的环节。
 
长期以来,只有一个基本的文本布局和渲染框架:CoreText。也有一个途径读取用户的键盘输入:UITextInput 协议。iOS6 甚至有一个途径来简单地获取系统的文本选择:继承 UITextView。
 
(这可能是重点,我应该公开我开发文本编辑器的十年经验了)在渲染文本和读取键盘输入之间存在着巨大(跟我读:巨大)的缺口。这个缺口可能也是导致很少有富文本或者语法高亮编辑器的原因了——毫无疑问,开发一个好用的文本编辑器得耗费几个月的时间。
 
就这样——如下是 iOS 文本(不那么)简短历史的简短概要:
 
iOS 2:这是第一个公开的 SDK,包括一个简单的文本显示组件( UILabel ),一个简单的文本输入组件( UITextField ),以及一个简单的、可滚动、可编辑的并且支持更大量文本的组件:UITextView。这些组件都只支持纯文本,没有文本选择支持(仅支持插入点),除了设置字体和文本颜色外几乎没有其他可定制功能。
 
iOS 3:新特性有复制和粘贴,以及复制粘贴所需要的文本选择功能。数据探测器(Data Detector)为文本视图提供了一个高亮电话号码和链接的方法。然而,除了打开或关闭这些特性外,开发者基本上没有什么别的事情可以做。
 
iOS 3.2:iPad 的出现带来了 CoreText,也就是前面提到的低级文本布局和渲染引擎(从Mac OS X 10.5 移植过来的),以及 UITextInput,前面也提到的键盘存取协议。Apple 将 Pages 作为移动设备上文本编辑功能的样板工程(附注1)。然而,由于我前面提到的框架缺口,只有很少的应用使用它们。
 
iOS 4:iOS 3.2 发布仅仅几个月后就发布了,文本方面没有一丁点新功能。(个人经历:在 WWDC,我走近工程师们,告诉他们我想要一个完善的 iOS 文本布局系统。回答是:“哦…提交个请求。”不出所料…)
 
iOS 5:文本方面没啥变化。(个人经历:在 WWDC,我和工程师们谈及 iOS 上文本系统。回答是:“我们没有看到太多的请求…” 靠!)
 
iOS 6:有些动作了:属性文本编辑被加入了UITextView。很不幸的是,它很难定制。默认的UI有粗体、斜体和下划线。用户可以设置字体大小和颜色。粗看起来相当不错,但还是没法控制布局或者提供一个便利的途径来定制文本属性。然而对于(文本编辑)开发者,有一个大的新功能:可以继承 UITextView 了,这样的话,除了以前版本提供的键盘输入外,开发者可以“免费”获得文本选择功能。必须实现一个完全自定义的文本选择功能,可能是很多对非纯文本工具开发的尝试半途而废的原因。(个人经历:我,WWDC,工程师们。我想要一个 iOS 的文本系统。回答:“嗯。吖。是的。也许?看,它只是不执行…” 所以毕竟还是有希望,对吧?)
 
iOS 7:终于来了,TextKit。
 
功能
 
所以咱们到了。iOS7 带着 TextKit 登陆了。咱们看看它可以做什么!深入之前,我还想提一下,严格来说,这些事情中的大部分以前都可以做。如果你有大量的资源和时间来用CoreText构建一个文本引擎,这些都是可以做的。但是如果以前你想构建一个完善的富文本编辑器,你得花费几个月的时间。现在就非常简单,你只需要到在Xcode里打开一个界面文件,然后将UITextView拖到你的试图控制器,就可以获得所有的功能:
 
字距调整(Kerning):所有的字符都有简单的二次的形状,这些形状必须被精确地放置,彼此相邻的,别这样想了。例如,现代文本布局会考虑到一个大写的“T”的“两翼”下面有一些空白,所以它会把后面的小写字母向左移让它们更靠近点。从而大大提高了文本的易读性,特别是在更长的文字中:
 
连写:我认为这主要是个艺术功能,但当某些字符组合(如“f”后面是“l”)使用组合符号(所谓的字形(glyph))绘制时,有些文本确实看起来更好(更美观)。
 
图像附件:现在可以在文本视图里面添加图像了。
 
断字:编辑文本时没那么重要,但如果要以好看易读的方式展现文本时,这就相当重要。断字意味着在行边界处分割单词,从而为整体文本创建一个更整齐的排版和外观。个人经历:iOS7 之前,开发者必须直接使用 CoreText。像这样:首先以句子为基础检测文本语言,然后获取句子中每个单词可能的断字点,然后在每一个可能的断字点上插入定制的连字占位字符。准备好之后,运行 CoreText 的布局方法并手动将连字符插入到断行。如果你想得到好的效果,之后你得检查带有连字符的文本没有超出行边界,如果超出了,在运行一次行的布局方法,这一次不要使用上次使用的断字点。使用 TextKit 的话,就非常简单了,设置 hyphenationFactor 属性就可以启用断字。
 
可定制性:对我来说,甚至比改进过的排版还多,这是个新的功能。以前开发者必须在使用现有的功能和自己全部重头写之间做出选择。现在提供了一整套类,它们有代理协议,或者可以被覆盖从而改变部分行为。例如,不必重写整个文本组件,你现在就可以改变指定单词的断行行为。我认为这是个胜利。
 
更多的富文本属性:现在可以设置不同的下划线样式(双线、粗线、虚线、点线,或者它们的组合)。提高文本的基线非常容易,这可用来设置上标数字。开发者也不再需要自己为定制渲染的文本绘制背景颜色了(CoreText 不支持这些功能)。
 
序列化:过去没有内置的方法从磁盘读取带文本属性的字符串。或者再写回磁盘。现在有了。
 
文本样式:iOS7 的界面引入了一个全局预定义的文本类型的新概念。这些文本类型分配了一个全局预定义的外观。理想情况下,这可以让整个系统的标题和连续文本具有一致的风格。通过设置应用,用户可以定义他们的阅读习惯(例如文本大小),那些使用文本样式的应用将自动拥有正确的文本大小和外观。
 
文本效果:最后也是最不重要的。iOS7 有且仅有一个文本效果:凸版。使用此效果的文本看起来像是盖在纸上面一样。内阴影,等等。个人观点:真的?靠…?在一个已经完全彻底不可饶恕地枪毙了所有无用的怀旧装饰的操作系统上,谁会需要这个像文本盖在纸上的外观?
 
结构
 
可能概览一个系统最好的方法是画一幅图。这是UIKit文本系统——TextKit的简图,:
从上图可以看出来,要让一个文本引擎工作,需要几个参与者。我们将从外到里介绍它们:
 
字符串(String):要绘制文本,那么必然在某个地方有个字符串存储它。在默认的结构中,NSTextStorage 保存并管理这个字符串,在这种情况中,它可以远离绘制。但并不一定非得这样。使用 TextKit 时,文本可以来自任何适合的来源。例如,对于一个代码编辑器,字符串可以是一棵包含所有显示的代码的结构信息的注释语法树(annotated syntax tree, AST)。使用一个定制的文本存储,这个文本只在后面动态地添加字体或颜色高亮等文本属性装饰。这是第一次,开发者可以直接为文本组件使用自己的模型。只需要一个特别设计的文本存储。即:
 
NSTextStorage:如果你把文本系统看做一个模型-视图-控制器(MVC)架构,这个类代表的是模型。文本存储是中心对象,它知道所有的文本和属性信息。它只提供了两个存取器方法存取它们,并提供了另外两个方法来修改它们。后面我们将进一步了解它们。现在重要的是你得理解 NSTextStorage 是从它的父类 NSAttributedString 继承了这些方法。这就很清楚了,文本存储——从文本系统看来——仅仅是一个带有属性的字符串,以及几个扩展。这两者唯一的重大不同点是文本存储包含了一个方法来发送内容改变的通知。我们会马上介绍这部分内容。
 
UITextView:堆栈的另一头是实际的视图。在 TextKit 中,文本视图有两个目的:第一,它是文本系统用来绘制的视图。文本视图它自己并不会做任何绘制;它仅仅提供一个供其它类绘制的区域。作为视图层级机构中唯一的组件,第二个目的是处理所有的用户交互。具体来说,文本视图实现 UITextInput 的协议来处理键盘事件,它为用户提供了一种途径来设置一个插入点或选择文本。它并不对文本做任何实际上的改变,仅仅将这些改变请求转发给刚刚讨论的文本存储。
 
NSTextContainer:每个文本视图定义了一个文本可以绘制的区域。为此,每个文本视图都有一个文本容器,它精确地描述了这个可用的区域。在简单的情况下,这是一个垂直的无限相当大的矩形区域。文本被填充到这个区域,并且文本视图允许用户滚动它。然而,在更高级的情况下,这个区域可能是一个无限大的矩形。例如,当渲染一本书时,每一页都有最大的高度和宽度。文本容器会定义这个大小,并且不接受任何超出的文本。相同情况下,一幅图像可能占据了页面的一部分,文本应该沿着它的边缘重新排版。这也是由文本容器来处理的,我们会在后面的例子中看到这一点。
 
NSLayoutManager:布局管理器是中心组件,它把所有组件粘合在一起:
1、这个管理器监听文本存储中文本或属性改变的通知,一旦接收到通知就触发布局进程。
2、从文本存储提供的文本开始,它将所有的字符翻译为字形(Glyph)(附注2).
3、一旦字形全部生成,这个管理器向它的文本容器(们)查询文本可用以绘制的区域
4、然后这些区域被行逐步填充,而行又被字形逐步填充。一旦一行填充完毕,下一行开始填充。
5、对于每一行,布局管理器必须考虑断行行为(放不下的单词必须移到下一行)、连字符、内联的图像附件等等。
6、当布局完成,文本的当前显示状态被设为无效,然后文本管理器将前面几步排版好的文本设给文本视图。
CoreText:没有直接包含在 TextKit 中,CoreText 是进行实际排版的库。对于布局管理器的每一步,CoreText 被这样或那样的方式调用。它提供了从字符到字形的翻译,用它们来填充行,以及建议断字点。
 
Cocoa 文本系统
 
创建像 TextKit 这样庞大复杂的系统肯定不是件简单快速的事情,而且肯定需要丰富的经验和知识。在 iOS 的前面6个主版本中,一直没有提供一个“真正的”文本组件,这也说明了这一点。Apple 把它视为一个大的新特性,当然没啥问题。但是它真的是全新的吗?
 
这里有个数字:在 UIKit 的 131 个公共类中,只有 9 个的名字没有使用UI作为前缀。这 9 个类使用的是旧系统的的、旧世界的(跟我读:Mac OS)前缀 NS。而且这九个类里面,有七个是用来处理文本的。巧合?好吧…
 
这是 Cocoa 文本系统的简图。不妨和上面 TextKit 的那幅图作一下对比。
惊人地相似。很明显,最起码主要部分,两者是相同的。很明显——除了右边部分以及 NSTextView 和 UITextView ——主要的类全部相同。TextKit 是(起码部分是)从 Cocoa 文本系统移植到 iOS。(我之前一直请求的那个,耶!)
 
进一步比较还是能看出一些不同的。最值得注意的有:
 
在 iOS 上没有 NSTypesetter 和 NSGlyphGenerator 这两个类。在 Mac OS 上有很多方法来定制排版,这被极大地简化了。这可以去掉一些抽象概念,并将这个过程合并到 NSLayoutManager 中来。保留下来的是少数的代理方法,以用来更改文本布局和断行行为。
 
这些类的 iOS 实现提供了几个新的而且非常便利的功能。在 Cocoa 中,必须手工地将确定的区域从文本容器分离出来(见上)。而 UIKit 类提供了一个简单的 exclusionPaths 属性就可以做到这一点。
 
有些功能未能提供,比如,内嵌表格,以及对非图像的附件的支持。
 
尽管有这些区别,总的来说系统还是一样的。NSTextStorage 在两个系统是是一模一样的,NSLayoutManager 和 NSTextContainer 也没有太大的不同。这些变动,在没有太多去除对一些特例的支持的情况下,看来(某些情况下大大地)使文本系统的使用变得更为容易。我认为这是件好事。
 
事后回顾我从 Apple 工程师那里得到的关于将 Cocoa 文本系统移植到 iOS 的答案,我们可以得到一些背景信息。拖到现在并削减功能的原因很简单:性能、性能、性能。文本布局可能是极度昂贵的任务——内存方面、电量方面以及时间方面——特别是在移动设备上。Apple 必须采用更简单的解决方案,并等到处理能力能够至少部分支持一个完善的文本布局引擎。
 
示例
 
为了说明 TextKit 的能力,我创建了一个小的演示项目,你可以在 GitHub 上找到它。在这个演示程序中,我只完成了一些以前不容易完成的功能。我必须承认编码工作只花了我礼拜天的一个上午的时间;如果以前要做同样的事情,我得花几天甚至几个星期。
 
TextKit 包括了超过 100 个方法,一篇文章根本没办法尽数涉及。而事实上,大多数时候,你需要的仅仅是一个正确的方法,TextKit 的使用和定制性也仍有待探索。所以我决定做四个更小的演示程序,而非一个大的演示程序来展示所有功能。每个演示程序中,我试着演示针对不同的方面和不同的类进行定制。
 
演示程序1:配置
 
让我们从最简单的开始:配置文本系统。正如你在上面 TextKit 简图中看到的,NSTextStorage、NSLayoutManager 和 NSTextContainer 之间的箭头都是有两个头的。我试图描述它们的关系是 1 对 N 的关系。就是那样:一个文本存储可以拥有多个布局管理器,一个布局管理器也可以拥有多个文本容器。这些多重性带来了很好的特性:
 
将多个文本管理器附加到一个文本存储上,可以产生相同文本的多种视觉表现,而且它们可以并排显示。每一个表现可以独立地布置和修改大小。如果相应的文本视图可编辑,那么在某个视图上做的所有修改都会马上反映到所有视图上。
 
将多个文本容器附加到一个文本管理器上,可以将一个文本分布到多个视图展现出来。例如很有用的基于页面的布局:每个页面包含一个单独的视图。一个文本管理器利用这些视图的文本容器,将文本分布到这些视图上。
 
在 storyboard 或者 interface 文件中实例化 UITextView 时,它会预配置一个文本系统:一个文本存储,引用一个文本管理器,而后者又引用一个文本容器。同样地,一个文本系统栈也可以通过代码直接创建:
  1. NSTextStorage *textStorage = [NSTextStorage new]; 
  2.   
  3. NSLayoutManager *layoutManager = [NSLayoutManager new]; 
  4. [textStorage addLayoutManager: layoutManager]; 
  5.   
  6. NSTextContainer *textContainer = [NSTextContainer new]; 
  7. [layoutManager addTextContainer: textContainer]; 
  8.   
  9. UITextView *textView = [[UITextView alloc] initWithFrame:someFrame  
  10.                                            textContainer:textContainer]; 
这是最简单的方式。手工创建一个文本系统,唯一需要记住的事情是你的视图控制器必须 retain 文本存储。在栈底的文本视图只保留了对文本存储和布局管理器的弱引用。当文本存储被释放时,布局管理器也被释放了,这样留给文本视图的就只有一个断开的容器了。
 
这个规则有一个例外。只有从一个 interface 文件或 storyboard 实例化一个文本视图时,文本视图确实会 retain 文本存储。框架使用了一些黑魔法以确保所有的对象都被 retain,而无需建立一个 retain 环。
 
记住这些之后,创建一个更高级的设置也非常简单。假设在一个视图里面依旧有一个从 nib 实例化的文本视图,叫做 originalTextView。增加对相同文本的第二个文本视图只需要复制上面的代码,并重用 originalTextView 的文本存储:
  1. NSTextStorage *sharedTextStorage = originalTextView.textStorage; 
  2.   
  3. NSLayoutManager *otherLayoutManager = [NSLayoutManager new]; 
  4. [sharedTextStorage addLayoutManager: otherLayoutManager]; 
  5.   
  6. NSTextContainer *otherTextContainer = [NSTextContainer new]; 
  7. [otherLayoutManager addTextContainer: otherTextContainer]; 
  8.   
  9. UITextView *otherTextView = [[UITextView alloc] initWithFrame:someFrame  
  10.                                                 textContainer:otherTextContainer]; 
 
将第二个文本容器附加到布局管理器也差不多。比方说我们希望上面例子中的文本填充两个文本视图,而非一个。简单:
  1. NSTextContainer *thirdTextContainer = [NSTextContainer new]; 
  2. [otherLayoutManager addTextContainer: thirdTextContainer]; 
  3.   
  4. UITextView *thirdTextView = [[UITextView alloc] initWithFrame:someFrame  
  5.                                                 textContainer:thirdTextContainer]; 
但有一点需要注意:由于在 otherTextView 中的文本容器可以无限地调整大小,thirdTextView 永远不会得到任何文本。因此,我们必须指定文本应该从一个视图回流到其它视图,而不应该调整大小或者滚动:
  1. otherTextView.scrollEnabled = NO; 
不幸的是,看来将多个文本容器附加到一个文本管理器会禁用编辑功能。如果必须保留编辑功能的话,你只可以将一个文本容器附加到一个文本管理器上。
 
想要一个这个配置的可运行的例子的话,请在前面提到的 TextKitDemo 中查看“Configuration”标签页。
 
演示程序2:语法高亮
 
如果配置文本视图不是那么令人激动,那么这里有更有趣的:语法高亮!
 
看看 TextKit 组件的责任划分,就很清楚语法高亮应该在文本存储上实现。因为 NSTextStorage 是一个类簇(附注3),创建它的子类需要做不少工作。我的想法是建立一个复合对象:实现所有的方法,但只是将对它们的调用转发给一个实际的实例,将输入输出参数或者结果修改为希望的样子。
 
NSTextStorage 继承自 NSMutableAttributedString,并且必须实现以下四个方法——两个 getter 和两个 setter:
  1. - (NSString *)string; 
  2. - (NSDictionary *)attributesAtIndex:(NSUInteger)location  
  3.                      effectiveRange:(NSRangePointer)range; 
  4. - (void)replaceCharactersInRange:(NSRange)range withString:(NSString *)str; 
  5. - (void)setAttributes:(NSDictionary *)attrs range:(NSRange)range; 
一个类簇的子类的复合对象的实现也相当简单。首先,找到一个满足所有要求的最简单的类。在我们的例子中,它是 NSMutableAttributedString,我们用它作为实现自定义存储的实现:
  1. @implementation TKDHighlightingTextStorage  
  2.     NSMutableAttributedString *_imp; 
  3.   
  4. - (id)init 
  5.     self = [super init]; 
  6.     if (self) { 
  7.         _imp = [NSMutableAttributedString new]; 
  8.     } 
  9.     return self; 
 有了这个对象,只需要一行代码就可以实现两个 getter 方法:
  1. - (NSString *)string  
  2.     return _imp.string; 
  3.   
  4. - (NSDictionary *)attributesAtIndex:(NSUInteger)location effectiveRange:(NSRangePointer)range  
  5.     return [_imp attributesAtIndex:location effectiveRange:range]; 
实现两个 setter 方法也几乎同样简单。但也有一个小麻烦:文本存储需要通知它的文本管理器变化发生了。因此 settter 方法必须也要调用 -edited:range:changeInLegth: 并传给它变化的描述。听起来更糟糕,实现变成:
  1. - (void)replaceCharactersInRange:(NSRange)range withString:(NSString *)str  
  2.     [_imp replaceCharactersInRange:range withString:str]; 
  3.     [self edited:NSTextStorageEditedCharacters range:range  
  4.                                       changeInLength:(NSInteger)str.length - (NSInteger)range.length]; 
  5.   
  6. - (void)setAttributes:(NSDictionary *)attrs range:(NSRange)range  
  7.     [_imp setAttributes:attrs range:range]; 
  8.     [self edited:NSTextStorageEditedAttributes range:range changeInLength:0]; 
就这样,我们在文本系统栈里面有了一个文本存储的全功能替换版本。在从 interface 文件中载入时,可以像这样将它插入文本视图——但是记住从一个实例变量引用文本存储:
  1. _textStorage = [TKDHighlightingTextStorage new]; 
  2. [_textStorage addLayoutManager: self.textView.layoutManager]; 
到目前为止,一切都很好。我们设法插入了一个自定义的文本存储,接下来我们需要真正高亮文本的某些部分了。现在,一个简单的高亮应该就是够了:我们希望将所有 iWords 的颜色变成红色——也就是那些以小写“i”开头,后面跟着一个大写字母的单词。
 
一个方便实现高亮的办法是覆盖 -processEditing。每次文本存储有修改时,这个方法都自动被调用。每次编辑后,NSTextStorage 会用这个方法来清理字符串。例如,有些字符无法用选定的字体显示时,文本存储使用一个可以显示它们的字体来进行替换。
 
和其它一样,为 iWords 增加一个简单的高亮也相当简单。我们覆盖 -processEditing,调用父类的实现,并设置一个正则表达式来查找单词:
  1. - (void)processEditing  
  2.     [super processEditing]; 
  3.   
  4.     static NSRegularExpression *iExpression; 
  5.     NSString *pattern = @"i[\\p{Alphabetic}&&\\p{Uppercase}][\\p{Alphabetic}]+"
  6.     iExpression = iExpression ?: [NSRegularExpression regularExpressionWithPattern:pattern  
  7.                                                                            options:0  
  8.                                                                              error:NULL]; 
然后,首先清除之前的所有高亮:
  1. NSRange paragaphRange = [self.string paragraphRangeForRange: self.editedRange]; 
  2. [self removeAttribute:NSForegroundColorAttributeName range:paragaphRange]; 
 
其次遍历所有的样式匹配项并高亮它们:
  1.  [iExpression enumerateMatchesInString:self.string  
  2.                                   options:0 range:paragaphRange  
  3.                                usingBlock:^(NSTextCheckingResult *result, NSMatchingFlags flags, BOOL *stop)  
  4.     { 
  5.         [self addAttribute:NSForegroundColorAttributeName value:[UIColor redColor] range:result.range]; 
  6.     }]; 
就是这样。我们创建了一个支持语法高亮的动态文本视图。当用户键入时,高亮将被实时应用。而且这只需几行代码。酷吧?
请注意仅仅使用 edited range 是不够的。例如,当手动键入 iWords,只有一个单词的第三个字符被键入后,正则表达式才开始匹配。但那时 editedRange 仅包含第三个字符,因此所有的处理只会检查这个字符。通过重新处理整个段落,我们可以完成高亮功能,又不会太过影响性能。
 
想要一个这个配置的可运行的例子的话,请在前面提到的 TextKitDemo 中查看“Highlighting”标签页。
 
演示程序3:布局修改
 
如前所述,布局管理器是核心的布局主力。Mac OS 上 NSTypesetter 的高度可定制功能被并入 iOS 上的 NSLayoutManager。虽然 TextKit 不具备像 Cocoa 文本系统那样的完全可定制性,但它提供很多代理方法来允许做一些调整。如前所述,TextKit 与 CoreText 更紧密地集成在一起,主要是基于性能方面的考虑。但是两个文本系统的理念在一定程度上是不一样的:
 
Cocoa 文本系统:在 Mac OS上,性能不是问题,设计考量的全部是灵活性。可能是这样:“这个东西可以做这个事情。如果你想的话,你可以覆盖它。性能不是问题。你也可以提供完全由自己实现的字符到字形的转换,去做吧…”
 
TextKit:性能看来真是个问题。理念(起码现在)更多的是像这样:“我们用简单但是高性能的方法实现了这个功能。这是结果,但是我们给你一个机会去更改它的一些东西。但是你只能在不太损害性能的地方进行修改。”
 
足够的理念,让我们来定制些东西。例如,调整行高如何?听起来不可思议,但是在之前的 iOS 发布版上调整行高至少是很黑客的行为,或者需要使用私有 API。幸运的是,现在(再一次)不用那么搞脑子了。设置布局管理器的代理并实现仅仅一个方法即可:
  1. - (CGFloat)      layoutManager:(NSLayoutManager *)layoutManager  
  2.   lineSpacingAfterGlyphAtIndex:(NSUInteger)glyphIndex  
  3.   withProposedLineFragmentRect:(CGRect)rect  
  4.     return floorf(glyphIndex / 100); 
在以上的代码中,我修改了行间距,让它与文本长度同时增长。这导致顶部的行比底部的行排列得更紧密。我承认这没什么实际的用处,但是它是可以做到的(而且肯定会有更实用的用例的)。
 
好,来一个更现实的场景。假设你的文本中有链接,你不希望这些链接被行包围。如果可能的话,一个 URL 应该始终显示为一个整体,一个单一的文本片段。没有什么比这更简单的了。
 
首先,我们通过使用自定义的文本存储,就像前面讨论过的那个。但是,它寻找链接并将其标记,而不是检测 iWords,如下:
  1. static NSDataDetector *linkDetector; 
  2. linkDetector = linkDetector ?: [[NSDataDetector alloc] initWithTypes:NSTextCheckingTypeLink error:NULL]; 
  3.   
  4. NSRange paragaphRange = [self.string paragraphRangeForRange: NSMakeRange(range.location, str.length)]; 
  5. [self removeAttribute:NSLinkAttributeName range:paragaphRange]; 
  6.   
  7. [linkDetector enumerateMatchesInString:self.string  
  8.                                options:0  
  9.                                  range:paragaphRange  
  10.                             usingBlock:^(NSTextCheckingResult *result, NSMatchingFlags flags, BOOL *stop)  
  11.     [self addAttribute:NSLinkAttributeName value:result.URL range:result.range]; 
  12. }]; 
 
有了这个,改变断行行为就只需要实现一个布局管理器的代理方法:
  1. - (BOOL)layoutManager:(NSLayoutManager *)layoutManager shouldBreakLineByWordBeforeCharacterAtIndex:(NSUInteger)charIndex  
  2.     NSRange range; 
  3.     NSURL *linkURL = [layoutManager.textStorage attribute:NSLinkAttributeName  
  4.                                                   atIndex:charIndex  
  5.                                            effectiveRange:&range]; 
  6.   
  7.     return !(linkURL && charIndex > range.location && charIndex <= NSMaxRange(range)); 
 
想要一个可运行的例子的话,请在前面提到的 TextKitDemo 中查看“Layout”标签页。以下是截屏:
顺便说一句,上面截屏里面的绿色轮廓线是无法用 TextKit 实现的。在这个演示程序中,我用了个小技巧来在布局管理器的子类中给文本画轮廓线。也可以很容易以特定的方法来扩展 TextKit 的绘制功能。一定要看看!
 
演示程序4:文本交互
 
前面已经涉及到了 NSTextStorage 和 NSLayoutManager,最后一个演示程序将涉及 NSTextContainer。这个类并不复杂,而且它除了指定文本可不可以放置在某个地方外,什么都没做。
 
不要将文本放置在某些区域,这是很常见的需求,例如,在杂志应用中。对于这种情况,iOS 上的 NSTextContainer 提供了一个 Mac 开发者梦寐以求的属性:exclusionPaths,它允许开发者设置一个 NSBezierPath 数组来指定不可填充文本的区域。要了解这到底是什么东西,看一眼下面的截屏:
 
正如你所看到的,所有的文本都放置在蓝色椭圆外面。在文本视图里面实现这个行为很简单,但是有个小麻烦:贝塞尔路径的坐标必须使用容器的坐标系。以下是转换方法:
  1. - (void)updateExclusionPaths  
  2.     CGRect ovalFrame = [self.textView convertRect:self.circleView.bounds  
  3.                                          fromView:self.circleView]; 
  4.   
  5.     ovalFrame.origin.x -= self.textView.textContainerInset.left; 
  6.     ovalFrame.origin.y -= self.textView.textContainerInset.top; 
  7.   
  8.     UIBezierPath *ovalPath = [UIBezierPath bezierPathWithOvalInRect:ovalFrame]; 
  9.     self.textView.textContainer.exclusionPaths = @[ovalPath]; 
在这个例子中,我使用了一个用户可移动的视图,它可以被自由移动,而文本会实时地围绕着它重新排版。我们首先将它的bounds(self.circleView.bounds)转换到文本视图的坐标系统。
 
因为没有 inset,文本会过于靠近视图边界,所以 UITextView 会在离边界还有几个点的距离的地方插入它的文本容器。因此,要得到以容器坐标表示的路径,必须从 origin 中减去这个插入点的坐标。
 
在此之后,只需将贝塞尔路径设置给文本容器即可将对应的区域排除掉。其它的过程对你来说是透明的,TextKit 会自动处理。
想要一个可运行的例子的话,请在前面提到的 TextKitDemo 中查看“Interaction”标签页。作为一个小噱头,它也包含了一个跟随当前文本选择的视图。应为,你也知道,没有一个小小的丑陋的烦人的回形针挡住你的话,那还是一个好的文本编辑器演示程序吗?
 
1. Pages 确实——据 Apple 声称——绝对没有使用私有 API。*咳* 我的理论:它要么使用了一个 TextKit 的史前版本,要么复制了 UIKit 一半的私有源程序。或者两者的混合。
2. 字形:如果说字符是一个字母的“语义”表达,字形则是它的可视化表达。取决于所使用的字体,字形要么是贝塞尔路径,或者位图图像,它定义了要绘制出来的形状。也请参考卓越的 Wikipedia 上关于字形的这篇文章
3. 在一个类簇中,只有一个抽象的父类是公共的。分配一个实例实际上就是创建其中一个私有类的对象。因此,你总是为一个抽象类创建子类,并且需要实现所有的方法。也请参考 class cluster documentation
 

原文链接:Max Seelemann  翻译:伯乐在线 - 和谐老约翰
译文链接:http://blog.jobbole.com/51965/



0 0
原创粉丝点击