TextKit详解

作者: iPhone | 来源:发表于2016-08-19 15:02 被阅读2625次

    一、参与者详解

    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 <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、布局演示
    需求:文本中的网址不断行
    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 () <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"//只是为椭圆添加一个空白边距
    
    @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
    
    

    相关文章

      网友评论

        本文标题:TextKit详解

        本文链接:https://www.haomeiwen.com/subject/joubsttx.html