
一、參與者詳解
1、string:讀入需要繪制的文本內容。
2、NSTextStorage:管理string的內容;這個很容易理解,NSTextStorage的父類是NSAttributedString繼承屬性文字所有的可設置屬性,但是他們唯一不同的地方在與:NSTextStorage包含了一個方法,可以將所有對其內容進行的修改以通知的方式發(fā)送出來(這個方法在后面會將到);簡單的理解就是:NSTextStorage保存并管理這個string;在使用一個自定義的 NSTextStorage 就可以讓文本在稍后動態(tài)地添加字體或者顏色高亮等文本屬性修飾。
3、UITextView:堆棧的另一頭是實際顯示的視圖。作用一,就是顯示內容,作用二,就是處理用戶的交互。唯一,需特別處理的就是,它已遵守了UITextInput的協(xié)議,來處理鍵盤事件。
4、NSTextContainer:textView給出了一個文本的繪制區(qū)域;在一般情況下,NSTextContainer精確的描述了這個可用的區(qū)域,其就是一個矩形,在垂直方向上無限大;但是,在特定的情況下,例如要是界面文字內容固定大小,就像是一本書一樣,每頁內容固定,可以翻頁的效果;還有一中情況就是,圖片在這個固定大小的頁面中占據(jù)了一塊區(qū)域,文字內容會,填充圖片意外剩余的區(qū)域。
5、NSLayoutManager:核心組件,聯(lián)系了以上所有組件;1、與NSTextStorage的關系:它監(jiān)聽著NSTextStorage發(fā)出的關于string屬性改變的通知,一旦接受到通知就會觸發(fā)重新布局;2、從NSTextStorage中獲取string(內容)將其轉化為字形(與當前設置的字體等內容相關);3、一旦字形完全生成完畢,NSLayoutManager(管理者)會像NSTextContainer查詢文本可用的繪制區(qū)域;4、NSTextContainer,會將文本的當前狀態(tài)改為無效,然后交給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 但是,使用了新創(chuàng)建的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:(分頁的實現(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、高亮文字
如果,不明白每個參與者的責任,你很難理解像textKit這樣的框架;例如,唐巧也很早寫過一篇博文,并在github配有Demo來講解textKit,但是,你看完要不是一臉懵逼,就是自己寫的話還是沒有邏輯;
廢話不多說,看代碼:在前面已經介紹了,各個參與者的責任,想要實現(xiàn)高亮文字,其實就是由NSTextStorage負責的,因為他繼承自NSMutableAttributedString;
NSTextStorage ---
NSTextStorage是NSMutableAttributedString的子類,根據(jù)蘋果官方文檔描述
是semiconcrete子類,因為NSTextStorage沒有實現(xiàn)
NSMutableAttributedString中的方法,所以說NSTextStorage應該是
NSMutableAttributedString的類簇。
所要我們深入使用NSTextStorage不僅要繼承NSTextStorage類還要實現(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
因為這些方法實際上NSTextStorage并沒有實現(xiàn)然而我們斷然不知道NSMutableAttributedString是如何實現(xiàn)這些方法,所以我們繼承NSTextStorage并實現(xiàn)這些方法最簡單的莫過于在NSTextStorage類中實例化一個NSMutableAttributedString對象然后調用NSMutableAttributedString對象的這些方法來實現(xiàn)NSTextStorage類中的這些方法
還值得注意的是:每次編輯都會調用-(void)processEditing的方法
-(void)processEditing;
完整的實現(xiàn)代碼如下:
.h文件
#import <UIKit/UIKit.h>
@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、布局演示
需求:文本中的網(wǎng)址不斷行
1.NSTextStorage負責監(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“對應的”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的代理做具體的實現(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、綜合實例
NSTextContainer 和NSBezierPath的使用
#import "TKDInteractionViewController.h"
#import "TKDCircleView.h"http://只是為橢圓添加一個空白邊距
@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