TextKit詳解

一、參與者詳解

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

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
【社區(qū)內容提示】社區(qū)部分內容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發(fā)布,文章內容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內容

友情鏈接更多精彩內容