• iOS富文本(三)深入使用Text Kit


    上一篇中介绍了Text Kit的三种基本组件的关系并且简单的实现了怎么使用这三种基本组件,本片将深入的去使用这三种基本组件。

    NSTextStorage

    NSTextStorageNSMutableAttributedString的子类,根据苹果官方文档描述是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类中的这些方法

    @interface LSYTextStorage : NSTextStorage
    @property (nonatomic,strong) NSMutableAttributedString *attributedString;
    @end

    继承NSTextStorage后都会实现下面的代码,如果要做一些特殊的处理知道在下面的代码里添加就可以了

    #import "LSYTextStorage.h"
    @interface LSYTextStorage ()
    @property (nonatomic,strong) NSMutableAttributedString *attributedString;
    @end
    @implementation LSYTextStorage
    - (instancetype)init
    {
        self = [super init];
        if (self) {
            _attributedString = [[NSMutableAttributedString alloc] init];
        }
        return self;
    }
    -(NSString *)string{
        return [_attributedString string];
    }
    - (NSDictionary *)attributesAtIndex:(NSUInteger)location effectiveRange:(nullable NSRangePointer)range
    {
        return [_attributedString attributesAtIndex:location effectiveRange:range];
    }
    -(void)replaceCharactersInRange:(NSRange)range withString:(NSString *)str
    {
        [self beginEditing];
        [_attributedString replaceCharactersInRange:range withString:str];
        [self edited:NSTextStorageEditedAttributes|NSTextStorageEditedCharacters range:range changeInLength:str.length-range.length];
        [self endEditing];
    }
    -(void)setAttributes:(NSDictionary<NSString *,id> *)attrs range:(NSRange)range
    {
        [self beginEditing];
        [_attributedString setAttributes:attrs range:range];
        [self edited:NSTextStorageEditedAttributes range:range changeInLength:0];
        [self endEditing];
    }
    @end
    

    上面实现的方法里代码都加上了beginEditingedited:range:changeInLength:endEditing的方法,这样做主要是通知它的 Layout Manager 发生了变化来计时调整布局
    根据上面提供的模版添加特殊处理的代码

    #import "LSYTextStorage.h"
    @interface LSYTextStorage ()
    @property (nonatomic,strong) NSMutableAttributedString *attributedString;
    @end
    @implementation LSYTextStorage
    - (instancetype)init
    {
        self = [super init];
        if (self) {
            _attributedString = [[NSMutableAttributedString alloc] init];
        }
        return self;
    }
    -(NSString *)string{
        return [_attributedString string];
    }
    - (NSDictionary *)attributesAtIndex:(NSUInteger)location effectiveRange:(nullable NSRangePointer)range
    {
        return [_attributedString attributesAtIndex:location effectiveRange:range];
    }
    -(void)replaceCharactersInRange:(NSRange)range withString:(NSString *)str
    {
        [self beginEditing];
        [_attributedString replaceCharactersInRange:range withString:str];
        [self edited:NSTextStorageEditedAttributes|NSTextStorageEditedCharacters range:range changeInLength:str.length-range.length];
        [self endEditing];
    }
    -(void)setAttributes:(NSDictionary<NSString *,id> *)attrs range:(NSRange)range
    {
        [self beginEditing];
        [_attributedString setAttributes:attrs range:range];
        [self edited:NSTextStorageEditedAttributes range:range changeInLength:0];
        [self endEditing];
    }
    -(void)processEditing
    {
    
        NSRange lineRange = NSUnionRange(self.editedRange, [self.string lineRangeForRange:self.editedRange]);  //正在编辑的整个段落范围
        [self.attributedString.string enumerateSubstringsInRange:lineRange options:NSStringEnumerationByWords usingBlock:^(NSString * _Nullable substring, NSRange substringRange, NSRange enclosingRange, BOOL * _Nonnull stop) {
            if ([substring isEqualToString:@"GGGHub"]) {
                [self setAttributes:@{NSForegroundColorAttributeName:[UIColor redColor]} range:substringRange];  //当出现GGGHub单词时字体变红
            }
            else{
                [self setAttributes:@{NSForegroundColorAttributeName:[UIColor blackColor]} range:substringRange]; //默认字体是黑色
            }
        }];
        [super processEditing];
    }
    @end

    每次编辑都会调用-(void)processEditing的方法,然后遍历整段修改的文字当出现GGGHub的单词时显示红色字体。

    #import "ViewController.h"
    #import "LSYTextStorage.h"
    @interface ViewController ()
    {
        LSYTextStorage *textStroage;  //需要声明为全局变量,否则出了作用域后就释放掉了
    }
    @property (weak, nonatomic) IBOutlet UITextView *textView;
    
    @end
    
    @implementation ViewController
    
    - (void)viewDidLoad {
        [super viewDidLoad];
        NSString *str = _textView.text;
        textStroage = [[LSYTextStorage alloc] init];
        [textStroage replaceCharactersInRange:NSMakeRange(0, 0) withString:str];
        [textStroage addLayoutManager:self.textView.layoutManager];
       //替换textView的textStroage属性
    }

    只要文本中出现指定的关键字字体就会变红,输入指定的关键字字体也会变红。

    实现效果

    代码在github NSTextStorage Tag下载


    NSLayoutManager

    布局管理器主要用来绘制字体的。NSTextStorage虽然能够改变字体的样式但是更改不了字体绘制的方式。我们可以继承NSLayoutManager来更改字体绘制。对于某些特定的字段可能不需要显示比如加密文本,或者用图片替换这些字段,或者給这些字段添加一些背景,这时只要重写NSLayoutManager中的某些方法可以很简单的实现。
    更改字体绘制与字体背景颜色只需要重写下面的两个方法

    - (void)drawBackgroundForGlyphRange:(NSRange)glyphsToShow atPoint:(CGPoint)origin;
    //绘制字形背景
    - (void)drawGlyphsForGlyphRange:(NSRange)glyphsToShow atPoint:(CGPoint)origin;
    //绘制字形

    下面例子会实现这样的功能,只要文本中有纯数字或者输入纯数字那么这段数字不显示出来,用黑色的遮罩挡住。
    首先在LSYTextStorage.m文件中更改processEditing函数

    -(void)processEditing
    {
    
        NSRange lineRange = NSUnionRange(self.editedRange, [self.string lineRangeForRange:self.editedRange]);
        NSString *regexNumber = @"^-?[0-9]\d*$";
        NSPredicate *predicateNumber = [NSPredicate predicateWithFormat:@"SELF MATCHES %@",regexNumber];
        //正则表达式,判断是否为纯数字
        [self.attributedString.string enumerateSubstringsInRange:lineRange options:NSStringEnumerationByWords usingBlock:^(NSString * _Nullable substring, NSRange substringRange, NSRange enclosingRange, BOOL * _Nonnull stop) {
            NSLog(@"%@",substring);
            if ([substring isEqualToString:@"GGGHub"]) {
                [self setAttributes:@{NSForegroundColorAttributeName:[UIColor redColor]} range:substringRange];
            }
            /**
             *  如果是纯数字給这段字符串添加LSYSecretAttribute属性为了绘制字形时查找
             */
            else if ([predicateNumber evaluateWithObject:substring]){
                [self setAttributes:@{@"LSYSecretAttribute":@"secretAttribute"} range:substringRange];
            }
            else{
                [self setAttributes:@{NSForegroundColorAttributeName:[UIColor blackColor]} range:substringRange];
            }
        }];
        [super processEditing];
    
    }

    上面的代码主要为纯数字的字符串添加一个LSYSecretAttribute的属性,当NSLayoutManager开始绘制字形时可以方便找到这段字符串然后在这段字符串的范围绘制黑色遮罩
    再重写NSLayoutManager的方法前

    下面重写NSLayoutManagerdrawGlyphsForGlyphRange方法

    -(void)drawGlyphsForGlyphRange:(NSRange)glyphsToShow atPoint:(CGPoint)origin
    {
    
        NSRange range = [self characterRangeForGlyphRange:glyphsToShow
                                         actualGlyphRange:NULL];
        [self.textStorage enumerateAttribute:@"LSYSecretAttribute" inRange:range options:0 usingBlock:^(id  _Nullable value, NSRange range, BOOL * _Nonnull stop) {
            //找到在LSYTextStorage中自定的LSYSecretAttribute属性
            if ([value isEqualToString:@"secretAttribute"]) {
                NSRange glyphRange = [self glyphRangeForCharacterRange:range
                                                  actualCharacterRange:NULL];
                NSTextContainer *
                container = [self textContainerForGlyphAtIndex:glyphRange.location
                                                effectiveRange:NULL];
                CGContextRef context = UIGraphicsGetCurrentContext();
                CGContextSaveGState(context);   //保存当前的绘图配置信息
                CGContextTranslateCTM(context, origin.x, origin.y); //转换初始坐标系到绘制字形的位置
                [[UIColor blackColor] setFill];
                CGRect rect = [self boundingRectForGlyphRange:glyphRange inTextContainer:container];
                [self drawSecret:rect]; //开始绘制
                 CGContextRestoreGState(context); //恢复绘图配置信息
            }
            else
            {
                [super drawGlyphsForGlyphRange:range atPoint:origin];
            }
    
        }];
    
    }

    实现效果

    这种遮罩是动态的,只要输入是纯数字那么NSLayoutManager的对象就不会对其进行绘制,而用黑色的遮罩挡住。
    代码在github NSLayoutManager Tag下载


  • 相关阅读:
    vue全家桶
    uniapp——如何配置scss和uview ui框架
    uniapp——自定义input清除事件
    响应式页面中的echart
    elementui 切换下拉框值,改变验证规则prop的表单项是否为必填项
    小程序view标签内容 文本过长,自动换行的问题
    vue 中使用图片查看器插件Viewer.js
    跳转不同导航,滚动条滚回初始
    vue项目中回显当前时间的农历时间
    移动端点击导航滑动展示全部选项,以为跳转页面定位到相应位置
  • 原文地址:https://www.cnblogs.com/itrena/p/5927103.html
Copyright © 2020-2023  润新知