一、参与者详解
1、string:读入需要绘制的文本内容。
2、NSTextStorage:管理string的内容;这个很容易理解,NSTextStorage的父类是NSAttributedString继承属性文字所有的可设置属性,但是他们唯一不同的地方在与:NSTextStorage包含了一个方法,可以将所有对其内容进行的修改以通知的方式发送出来(这个方法在后面会将到);简单的理解就是:NSTextStorage保存并管理这个string;在使用一个自定义的 NSTextStorage 就可以让文本在稍后动态地添加字体或者颜色高亮等文本属性修饰。
3、UITextView:堆栈的另一头是实际显示的视图。作用一,就是显示内容,作用二,就是处理用户的交互。唯一,需特别处理的就是,它已遵守了UITextInput的协议,来处理键盘事件。
4、NSTextContainer:textView给出了一个文本的绘制区域;在一般情况下,NSTextContainer精确的描述了这个可用的区域,其就是一个矩形,在垂直方向上无限大;但是,在特定的情况下,例如要是界面文字内容固定大小,就像是一本书一样,每页内容固定,可以翻页的效果;还有一中情况就是,图片在这个固定大小的页面中占据了一块区域,文字内容会,填充图片意外剩余的区域。
5、NSLayoutManager:核心组件,联系了以上所有组件;1、与NSTextStorage的关系:它监听着NSTextStorage发出的关于string属性改变的通知,一旦接受到通知就会触发重新布局;2、从NSTextStorage中获取string(内容)将其转化为字形(与当前设置的字体等内容相关);3、一旦字形完全生成完毕,NSLayoutManager(管理者)会像NSTextContainer查询文本可用的绘制区域;4、NSTextContainer,会将文本的当前状态改为无效,然后交给textView去显示。
注:CoreText,并没用直接包含在TextKit中,CoreText是进行实地排版的库,他详细的管理者实地排版中的每一行,断句以及从字义到字形的翻译。
二、Demo
Demo1、基本用法
- (void)viewDidLoad
{
[super viewDidLoad];
//1、获取文本管理者
NSTextStorage *sharedTextStorage = self.originalTextView.textStorage;
//2、读取本地文件
[sharedTextStorage replaceCharactersInRange:NSMakeRange(0, 0) withString:[NSString stringWithContentsOfURL:[NSBundle.mainBundle URLForResource:@"lorem" withExtension:@"txt"] usedEncoding:NULL error:NULL]];
//3、布局与字形的管理
NSLayoutManager *otherLayoutManager = [NSLayoutManager new];
[sharedTextStorage addLayoutManager: otherLayoutManager];
//4、布局的rect
NSTextContainer *otherTextContainer = [NSTextContainer new];
[otherLayoutManager addTextContainer: otherTextContainer];
//otherTextView与originalTextView使用了同一个NSTextStorage 但是,使用了新创建的NSLayoutManager与NSTextContainer独立管理otherTextView的布局
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;
//thirdTextView与otherTextView使用了同一个otherLayoutManager:(分页的实现)
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;
}
- (IBAction)endEditing:(UIBarButtonItem *)sender
{
[self.view endEditing: YES];
}
Demo2、高亮文字
如果,不明白每个参与者的责任,你很难理解像textKit这样的框架;例如,唐巧也很早写过一篇博文,并在github配有Demo来讲解textKit,但是,你看完要不是一脸懵逼,就是自己写的话还是没有逻辑;
废话不多说,看代码:在前面已经介绍了,各个参与者的责任,想要实现高亮文字,其实就是由NSTextStorage负责的,因为他继承自NSMutableAttributedString;
NSTextStorage ---
NSTextStorage是NSMutableAttributedString的子类,根据苹果官方文档描述
是semiconcrete子类,因为NSTextStorage没有实现
NSMutableAttributedString中的方法,所以说NSTextStorage应该是
NSMutableAttributedString的类簇。
所要我们深入使用NSTextStorage不仅要继承NSTextStorage类还要实现
NSMutableAttributedString的下面方法
- (NSString *)string
- (void)replaceCharactersInRange:(NSRange)range withString:(NSString *)str
- (NSDictionary *)attributesAtIndex:(NSUInteger)location effectiveRange:(NSRangePointer)range
- (void)setAttributes:(NSDictionary *)attrs range:(NSRange)range
因为这些方法实际上NSTextStorage并没有实现然而我们断然不知道NSMutableAttributedString是如何实现这些方法,所以我们继承NSTextStorage并实现这些方法最简单的莫过于在NSTextStorage类中实例化一个NSMutableAttributedString对象然后调用NSMutableAttributedString对象的这些方法来实现NSTextStorage类中的这些方法
还值得注意的是:每次编辑都会调用-(void)processEditing的方法
-(void)processEditing;
完整的实现代码如下:
.h文件
#import
@interface TKDHighlightingTextStorage : NSTextStorage
@end
.m文件
#import "TKDHighlightingTextStorage.h"
@implementation TKDHighlightingTextStorage
{
NSMutableAttributedString *_imp;
}
//实例化 NSMutableAttributedString对象
- (id)init
{
self = [super init];
if (self) {
_imp = [NSMutableAttributedString new];
}
return self;
}
#pragma mark - Reading Text - get方法
- (NSString *)string
{
return _imp.string;
}
- (NSDictionary *)attributesAtIndex:(NSUInteger)location effectiveRange:(NSRangePointer)range
{
return [_imp attributesAtIndex:location effectiveRange:range];
}
#pragma mark - Text Editing
- (void)replaceCharactersInRange:(NSRange)range withString:(NSString *)str
{
[_imp replaceCharactersInRange:range withString:str];
[self edited:NSTextStorageEditedCharacters range:range changeInLength:(NSInteger)str.length - (NSInteger)range.length];
}
- (void)setAttributes:(NSDictionary *)attrs range:(NSRange)range
{
[_imp setAttributes:attrs range:range];
[self edited:NSTextStorageEditedAttributes range:range changeInLength:0];
}
#pragma mark - Syntax highlighting
- (void)processEditing
{
//正则表达式来查找单词以i开头连接W的单词
static NSRegularExpression *iExpression;
iExpression = iExpression ?: [NSRegularExpression regularExpressionWithPattern:@"i[\\p{Alphabetic}&&\\p{Uppercase}][\\p{Alphabetic}]+" options:0 error:NULL];
// 首先清除之前的所有高亮
NSRange paragaphRange = [self.string paragraphRangeForRange: self.editedRange];
[self removeAttribute:NSForegroundColorAttributeName range:paragaphRange];
// 其次遍历所有的样式匹配项并高亮它们
[iExpression enumerateMatchesInString:self.string options:0 range:paragaphRange usingBlock:^(NSTextCheckingResult *result, NSMatchingFlags flags, BOOL *stop) {
// Add red highlight color
[self addAttribute:NSForegroundColorAttributeName value:[UIColor redColor] range:result.range];
}];
/*
请注意仅仅使用 edited range 是不够的。例如,当手动键入 iWords,只有一个单词的第三个字符被键入后,正则表达式才开始匹配。但那时 editedRange 仅包含第三个字符,因此所有的处理只会影响这一个字符。通过重新处理整个段落可以解决这个问题,这样既完成高亮功能,又不会太过影响性能
*/
[super processEditing];
}
@end
Demo3、布局演示
需求:文本中的网址不断行
1.NSTextStorage负责监听文本中出现的网址string
#import "TKDLinkDetectingTextStorage.h"
@implementation TKDLinkDetectingTextStorage
{
NSTextStorage *_imp;
}
- (id)init
{
self = [super init];
if (self) {
_imp = [NSTextStorage new];
}
return self;
}
#pragma mark - Reading Text
- (NSString *)string
{
return _imp.string;
}
- (NSDictionary *)attributesAtIndex:(NSUInteger)location effectiveRange:(NSRangePointer)range
{
return [_imp attributesAtIndex:location effectiveRange:range];
}
#pragma mark - Text Editing
//NSString 替换字符串中某一位置的文字
- (void)replaceCharactersInRange:(NSRange)range withString:(NSString *)str
{
// Normal replace
[_imp replaceCharactersInRange:range withString:str];
[self edited:NSTextStorageEditedCharacters range:range changeInLength:(NSInteger)str.length - (NSInteger)range.length];
// Regular expression matching all iWords -- first character i, followed by an uppercase alphabetic character, followed by at least one other character. Matches words like iPod, iPhone, etc.
static NSDataDetector *linkDetector;
linkDetector = linkDetector ?: [[NSDataDetector alloc] initWithTypes:NSTextCheckingTypeLink error:NULL];
// Clear text color of edited range
NSRange paragaphRange = [self.string paragraphRangeForRange: NSMakeRange(range.location, str.length)];
[self removeAttribute:NSLinkAttributeName range:paragaphRange];
[self removeAttribute:NSBackgroundColorAttributeName range:paragaphRange];
[self removeAttribute:NSUnderlineStyleAttributeName range:paragaphRange];
// Find all iWords in range
[linkDetector enumerateMatchesInString:self.string options:0 range:paragaphRange usingBlock:^(NSTextCheckingResult *result, NSMatchingFlags flags, BOOL *stop) {
// Add red highlight color
[self addAttribute:NSLinkAttributeName value:result.URL range:result.range];
[self addAttribute:NSBackgroundColorAttributeName value:[UIColor yellowColor] range:result.range];
[self addAttribute:NSUnderlineStyleAttributeName value:@(NSUnderlineStyleSingle) range:result.range];
}];
}
- (void)setAttributes:(NSDictionary *)attrs range:(NSRange)range
{
[_imp setAttributes:attrs range:range];
[self edited:NSTextStorageEditedAttributes range:range changeInLength:0];
}
@end
2.重写NSLayoutManager“对应的”drawGlyphsForGlyphRange方法
这里我们重写这个方法
#import "TKDOutliningLayoutManager.h"
@implementation TKDOutliningLayoutManager
//下面重写NSLayoutManager的drawGlyphsForGlyphRange方法
- (void)drawUnderlineForGlyphRange:(NSRange)glyphRange underlineType:(NSUnderlineStyle)underlineVal baselineOffset:(CGFloat)baselineOffset lineFragmentRect:(CGRect)lineRect lineFragmentGlyphRange:(NSRange)lineGlyphRange containerOrigin:(CGPoint)containerOrigin
{
// Left border (== position) of first underlined glyph
CGFloat firstPosition = [self locationForGlyphAtIndex: glyphRange.location].x;
// Right border (== position + width) of last underlined glyph
CGFloat lastPosition;
// When link is not the last text in line, just use the location of the next glyph
if (NSMaxRange(glyphRange) < NSMaxRange(lineGlyphRange)) {
lastPosition = [self locationForGlyphAtIndex: NSMaxRange(glyphRange)].x;
}
// Otherwise get the end of the actually used rect
else {
lastPosition = [self lineFragmentUsedRectForGlyphAtIndex:NSMaxRange(glyphRange)-1 effectiveRange:NULL].size.width;
}
// Inset line fragment to underlined area
lineRect.origin.x += firstPosition;
lineRect.size.width = lastPosition - firstPosition;
// Offset line by container origin
lineRect.origin.x += containerOrigin.x;
lineRect.origin.y += containerOrigin.y;
// Align line to pixel boundaries, passed rects may be
lineRect = CGRectInset(CGRectIntegral(lineRect), .5, .5);
[[UIColor greenColor] set];
[[UIBezierPath bezierPathWithRect: lineRect] stroke];
}
3.在textView所在页面,使用NSLayoutManager的代理做具体的实现
#import "TKDLayoutingViewController.h"
#import "TKDLinkDetectingTextStorage.h"
#import "TKDOutliningLayoutManager.h"
@interface TKDLayoutingViewController ()
{
// Text storage must be held strongly, only the default storage is retained by the text view.
TKDLinkDetectingTextStorage *_textStorage;
}
@end
@implementation TKDLayoutingViewController
- (void)viewDidLoad
{
[super viewDidLoad];
// Create componentes
_textStorage = [TKDLinkDetectingTextStorage new];
NSLayoutManager *layoutManager = [TKDOutliningLayoutManager new];
[_textStorage addLayoutManager: layoutManager];
NSTextContainer *textContainer = [[NSTextContainer alloc] initWithSize: CGSizeZero];
[layoutManager addTextContainer: textContainer];
UITextView *textView = [[UITextView alloc] initWithFrame:CGRectInset(self.view.bounds, 5, 20) textContainer: textContainer];
textView.autoresizingMask = UIViewAutoresizingFlexibleHeight | UIViewAutoresizingFlexibleWidth;
textView.translatesAutoresizingMaskIntoConstraints = YES;
textView.keyboardDismissMode = UIScrollViewKeyboardDismissModeInteractive;
[self.view addSubview: textView];
// Set delegate
layoutManager.delegate = self;
// Load layout text
[_textStorage replaceCharactersInRange:NSMakeRange(0, 0) withString:[NSString stringWithContentsOfURL:[NSBundle.mainBundle URLForResource:@"layout" withExtension:@"txt"] usedEncoding:NULL error:NULL]];
}
#pragma mark - Layout
- (BOOL)layoutManager:(NSLayoutManager *)layoutManager shouldBreakLineByWordBeforeCharacterAtIndex:(NSUInteger)charIndex
{
NSRange range;
NSURL *linkURL = [layoutManager.textStorage attribute:NSLinkAttributeName atIndex:charIndex effectiveRange:&range];
// Do not break lines in links unless absolutely required
if (linkURL && charIndex > range.location && charIndex <= NSMaxRange(range))
return NO;
else
return YES;
}
- (CGFloat)layoutManager:(NSLayoutManager *)layoutManager lineSpacingAfterGlyphAtIndex:(NSUInteger)glyphIndex withProposedLineFragmentRect:(CGRect)rect
{
return floorf(glyphIndex / 100);
}
- (CGFloat)layoutManager:(NSLayoutManager *)layoutManager paragraphSpacingAfterGlyphAtIndex:(NSUInteger)glyphIndex withProposedLineFragmentRect:(CGRect)rect
{
return 10;
}
@end
Demo4、综合实例
NSTextContainer 和NSBezierPath的使用
#import "TKDInteractionViewController.h"
#import "TKDCircleView.h"//只是为椭圆添加一个空白边距
@interface TKDInteractionViewController ()
{
CGPoint _panOffset;
}
@end
@implementation TKDInteractionViewController
- (void)viewDidLoad
{
[super viewDidLoad];
// Load text
[self.textView.textStorage replaceCharactersInRange:NSMakeRange(0, 0) withString:[NSString stringWithContentsOfURL:[NSBundle.mainBundle URLForResource:@"lorem" withExtension:@"txt"] usedEncoding:NULL error:NULL]];
// Delegate
self.textView.delegate = self;
self.clippyView.hidden = YES;
// Set up circle pan
[self.circleView addGestureRecognizer: [[UIPanGestureRecognizer alloc] initWithTarget:self action:@selector(circlePan:)]];
[self updateExclusionPaths];
// Enable hyphenation
self.textView.layoutManager.hyphenationFactor = 1.0;
}
#pragma mark - Exclusion
- (void)circlePan:(UIPanGestureRecognizer *)pan
{
// Capute offset in view on begin
if (pan.state == UIGestureRecognizerStateBegan)
_panOffset = [pan locationInView: self.circleView];
// Update view location
CGPoint location = [pan locationInView: self.view];
CGPoint circleCenter = self.circleView.center;
circleCenter.x = location.x - _panOffset.x + self.circleView.frame.size.width / 2;
circleCenter.y = location.y - _panOffset.y + self.circleView.frame.size.width / 2;
self.circleView.center = circleCenter;
// Update exclusion path
[self updateExclusionPaths];
}
- (void)updateExclusionPaths
{
CGRect ovalFrame = [self.textView convertRect:self.circleView.bounds fromView:self.circleView];
// Since text container does not know about the inset, we must shift the frame to container coordinates
ovalFrame.origin.x -= self.textView.textContainerInset.left;
ovalFrame.origin.y -= self.textView.textContainerInset.top;
// Simply set the exclusion path
UIBezierPath *ovalPath = [UIBezierPath bezierPathWithOvalInRect: ovalFrame];
self.textView.textContainer.exclusionPaths = @[ovalPath];
// And don't forget clippy
[self updateClippy];
}
#pragma mark - Selection tracking
- (void)textViewDidChangeSelection:(UITextView *)textView
{
[self updateClippy];
}
- (void)updateClippy
{
// Zero length selection hide clippy
NSRange selectedRange = self.textView.selectedRange;
if (!selectedRange.length) {
self.clippyView.hidden = YES;
return;
}
// Find last rect of selection
NSRange glyphRange = [self.textView.layoutManager glyphRangeForCharacterRange:selectedRange actualCharacterRange:NULL];
__block CGRect lastRect;
[self.textView.layoutManager enumerateEnclosingRectsForGlyphRange:glyphRange withinSelectedGlyphRange:glyphRange inTextContainer:self.textView.textContainer usingBlock:^(CGRect rect, BOOL *stop) {
lastRect = rect;
}];
// Position clippy at bottom-right of selection
CGPoint clippyCenter;
clippyCenter.x = CGRectGetMaxX(lastRect) + self.textView.textContainerInset.left;
clippyCenter.y = CGRectGetMaxY(lastRect) + self.textView.textContainerInset.top;
clippyCenter = [self.textView convertPoint:clippyCenter toView:self.view];
clippyCenter.x += self.clippyView.bounds.size.width / 2;
clippyCenter.y += self.clippyView.bounds.size.height / 2;
self.clippyView.hidden = NO;
self.clippyView.center = clippyCenter;
}
@end