一敌厘、參與者詳解
1滴须、string:讀入需要繪制的文本內(nèi)容舌狗。
2、NSTextStorage:管理string的內(nèi)容扔水;這個(gè)很容易理解痛侍,NSTextStorage的父類是NSAttributedString繼承屬性文字所有的可設(shè)置屬性,但是他們唯一不同的地方在與:NSTextStorage包含了一個(gè)方法铭污,可以將所有對(duì)其內(nèi)容進(jìn)行的修改以通知的方式發(fā)送出來(這個(gè)方法在后面會(huì)將到)恋日;簡(jiǎn)單的理解就是:NSTextStorage保存并管理這個(gè)string;在使用一個(gè)自定義的 NSTextStorage 就可以讓文本在稍后動(dòng)態(tài)地添加字體或者顏色高亮等文本屬性修飾嘹狞。
3、UITextView:堆棧的另一頭是實(shí)際顯示的視圖誓竿。作用一磅网,就是顯示內(nèi)容,作用二筷屡,就是處理用戶的交互涧偷。唯一,需特別處理的就是毙死,它已遵守了UITextInput的協(xié)議燎潮,來處理鍵盤事件。
4扼倘、NSTextContainer:textView給出了一個(gè)文本的繪制區(qū)域确封;在一般情況下,NSTextContainer精確的描述了這個(gè)可用的區(qū)域再菊,其就是一個(gè)矩形爪喘,在垂直方向上無限大;但是纠拔,在特定的情況下秉剑,例如要是界面文字內(nèi)容固定大小,就像是一本書一樣稠诲,每頁內(nèi)容固定侦鹏,可以翻頁的效果;還有一中情況就是臀叙,圖片在這個(gè)固定大小的頁面中占據(jù)了一塊區(qū)域略水,文字內(nèi)容會(huì),填充圖片意外剩余的區(qū)域匹耕。
5聚请、NSLayoutManager:核心組件,聯(lián)系了以上所有組件;1驶赏、與NSTextStorage的關(guān)系:它監(jiān)聽著NSTextStorage發(fā)出的關(guān)于string屬性改變的通知炸卑,一旦接受到通知就會(huì)觸發(fā)重新布局;2煤傍、從NSTextStorage中獲取string(內(nèi)容)將其轉(zhuǎn)化為字形(與當(dāng)前設(shè)置的字體等內(nèi)容相關(guān))盖文;3、一旦字形完全生成完畢蚯姆,NSLayoutManager(管理者)會(huì)像NSTextContainer查詢文本可用的繪制區(qū)域五续;4、NSTextContainer龄恋,會(huì)將文本的當(dāng)前狀態(tài)改為無效疙驾,然后交給textView去顯示。
注:CoreText,并沒用直接包含在TextKit中郭毕,CoreText是進(jìn)行實(shí)地排版的庫它碎,他詳細(xì)的管理者實(shí)地排版中的每一行,斷句以及從字義到字形的翻譯显押。
二扳肛、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使用了同一個(gè)NSTextStorage 但是套腹,使用了新創(chuàng)建的NSLayoutManager與NSTextContainer獨(dú)立管理otherTextView的布局
UITextView *otherTextView = [[UITextView alloc] initWithFrame:self.otherContainerView.bounds textContainer:otherTextContainer];
otherTextView.backgroundColor = self.otherContainerView.backgroundColor;
otherTextView.translatesAutoresizingMaskIntoConstraints = YES;
otherTextView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
//禁止滑動(dòng)
otherTextView.scrollEnabled = NO;
[self.otherContainerView addSubview: otherTextView];
self.otherTextView = otherTextView;
//thirdTextView與otherTextView使用了同一個(gè)otherLayoutManager:(分頁的實(shí)現(xiàn))
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、高亮文字
如果轿衔,不明白每個(gè)參與者的責(zé)任沉迹,你很難理解像textKit這樣的框架;例如害驹,唐巧也很早寫過一篇博文鞭呕,并在github配有Demo來講解textKit,但是宛官,你看完要不是一臉懵逼葫松,就是自己寫的話還是沒有邏輯;
廢話不多說底洗,看代碼:在前面已經(jīng)介紹了腋么,各個(gè)參與者的責(zé)任,想要實(shí)現(xiàn)高亮文字亥揖,其實(shí)就是由NSTextStorage負(fù)責(zé)的珊擂,因?yàn)樗^承自NSMutableAttributedString圣勒;
NSTextStorage ---
NSTextStorage是NSMutableAttributedString的子類,根據(jù)蘋果官方文檔描述
是semiconcrete子類摧扇,因?yàn)镹STextStorage沒有實(shí)現(xiàn)
NSMutableAttributedString中的方法圣贸,所以說NSTextStorage應(yīng)該是
NSMutableAttributedString的類簇。
所要我們深入使用NSTextStorage不僅要繼承NSTextStorage類還要實(shí)現(xiàn)
NSMutableAttributedString的下面方法
- (NSString *)string
- (void)replaceCharactersInRange:(NSRange)range withString:(NSString *)str
- (NSDictionary *)attributesAtIndex:(NSUInteger)location effectiveRange:(NSRangePointer)range
- (void)setAttributes:(NSDictionary *)attrs range:(NSRange)range
因?yàn)檫@些方法實(shí)際上NSTextStorage并沒有實(shí)現(xiàn)然而我們斷然不知道NSMutableAttributedString是如何實(shí)現(xiàn)這些方法扛稽,所以我們繼承NSTextStorage并實(shí)現(xiàn)這些方法最簡(jiǎn)單的莫過于在NSTextStorage類中實(shí)例化一個(gè)NSMutableAttributedString對(duì)象然后調(diào)用NSMutableAttributedString對(duì)象的這些方法來實(shí)現(xiàn)NSTextStorage類中的這些方法
還值得注意的是:每次編輯都會(huì)調(diào)用-(void)processEditing的方法
-(void)processEditing;
完整的實(shí)現(xiàn)代碼如下:
.h文件
#import <UIKit/UIKit.h>
@interface TKDHighlightingTextStorage : NSTextStorage
@end
.m文件
#import "TKDHighlightingTextStorage.h"
@implementation TKDHighlightingTextStorage
{
NSMutableAttributedString *_imp;
}
//實(shí)例化 NSMutableAttributedString對(duì)象
- (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
{
//正則表達(dá)式來查找單詞以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];
// 其次遍歷所有的樣式匹配項(xiàng)并高亮它們
[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];
}];
/*
請(qǐng)注意僅僅使用 edited range 是不夠的吁峻。例如,當(dāng)手動(dòng)鍵入 iWords在张,只有一個(gè)單詞的第三個(gè)字符被鍵入后用含,正則表達(dá)式才開始匹配。但那時(shí) editedRange 僅包含第三個(gè)字符帮匾,因此所有的處理只會(huì)影響這一個(gè)字符啄骇。通過重新處理整個(gè)段落可以解決這個(gè)問題,這樣既完成高亮功能辟狈,又不會(huì)太過影響性能
*/
[super processEditing];
}
@end
Demo3肠缔、布局演示
需求:文本中的網(wǎng)址不斷行
1.NSTextStorage負(fù)責(zé)監(jiān)聽文本中出現(xiàn)的網(wǎng)址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“對(duì)應(yīng)的”drawGlyphsForGlyphRange方法
這里我們重寫這個(gè)方法
#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的代理做具體的實(shí)現(xiàn)
#import "TKDLayoutingViewController.h"
#import "TKDLinkDetectingTextStorage.h"
#import "TKDOutliningLayoutManager.h"
@interface TKDLayoutingViewController () <NSLayoutManagerDelegate>
{
// 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哼转、綜合實(shí)例
NSTextContainer 和NSBezierPath的使用
#import "TKDInteractionViewController.h"
#import "TKDCircleView.h"http://只是為橢圓添加一個(gè)空白邊距
@interface TKDInteractionViewController () <UITextViewDelegate>
{
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