iOS实录3:扩展UITextView的特性

[这是第三篇]

导语:虽然app大部分时间在展示数据,但是有些场景下,app也是数据输入的入口。此种场景下,UITextField 和UITextView用得就比较多。这里重点说一下TextView。结合之前遇到的需求,定制了一个TextView,增加了一些新特性。

新增的特性

在实际开发中,遇到些需求,UITextView原来的特性不够用,需要自己定制一些特性。我采用的方式是创建一个UITextView子类QSTextView。为QSTextView添加若干特性,这些特性有

1、支持placeholder。这个在业务中出现频率比较高。(好奇iOS默认的UITextView不支持这个小特性)。
2、输入时自适应高度变化,高度增长方向可控制。这个在编辑大段文本情况下遇到比较多。
3、设置边框:无(默认)、虚线、实线。
4、输入文本时候,自动识别url,并可以点击打开url。

一、支持placeholder的实现

1、在QSTextView中定义了一个placeholderView
- (UITextView *)placeholderView{

    if (!_placeholderView) {
        _placeholderView = [[UITextView alloc] init];
        _placeholderView.scrollEnabled = NO;
        _placeholderView.showsHorizontalScrollIndicator = NO;
        _placeholderView.showsVerticalScrollIndicator = NO;
        _placeholderView.userInteractionEnabled = NO;
        _placeholderView.font = self.font;
        _placeholderView.textColor = [UIColor lightGrayColor];
        _placeholderView.backgroundColor = [UIColor clearColor];
        [self addSubview:_placeholderView];
  }
  return _placeholderView;

}

2、控制placeholderView的显隐和frame
//实现和隐藏
self.placeholderView.hidden = self.text.length > 0;

//frame变化
- (void)adjustPlaceholderFrame{

    CGFloat height = ceil(self.placeholderFont.lineHeight + self.textContainerInset.top + self.textContainerInset.bottom);
    self.placeholderView.frame = CGRectMake(self.bounds.origin.x, self.bounds.origin.y, self.frame.size.width, height);
}
3、我们也支持设置placeholde的文本、颜色和字体
#pragma mark - 占位placeholder相关的属性
/**
 *  占位文字
 */
@property (nonatomic, strong) NSString *placeholder;

 /**
 *  占位文字颜色
 */
@property (nonatomic, strong) UIColor *placeholderColor;

 /**
 占位文字字体
 */
@property (nonatomic, strong) UIFont *placeholderFont;

二、输入时自适应高度变化,高度增长方向可控制

1、添加高度相关的属性
/**
 QSTextView的增长方向
 */
typedef NS_ENUM(NSInteger,QSTextViewGrowDirection){
    QSTextViewGrowDirectionDown = 0, //向下,默认
    QSTextViewGrowDirectionUp = 1,   //向上
};

#pragma mark - 高度相关的属性

/**
 增长方向
 */
@property (nonatomic,assign)QSTextViewGrowDirection growDirection;

/**
 *  textView最大行数
 */
@property (nonatomic, assign) NSUInteger maxNumberOfLines;

/**
 *  textView的高度
 */
@property (nonatomic, assign,readonly) NSInteger textViewHeight;
2、监听文本变化,更新高度
- (void)textDidChange{

    // 占位文字是否显示
    self.placeholderView.hidden = self.text.length > 0;
    if (self.placeholderView.hidden != YES) {
        [self adjustPlaceholderFrame];
    }

    //获取输入文本
    if (CHECK_VALID_DELEGATE(self.qsDelegate, @selector(textView:textChange:))) {
        [self.qsDelegate textView:self textChange:self.text];
    }

    //计算当前view的高度
    NSInteger calcHeight = ceilf([self sizeThatFits:CGSizeMake(self.bounds.size.width, MAXFLOAT)].height);

    if (_textViewHeight != calcHeight && calcHeight >= _originHeight) { // 高度不一样,就改变了高度
    
        //更新高度
        _textViewHeight = calcHeight;
    
        if (calcHeight > _maxTextViewHeight && _maxTextViewHeight > 0) {
            self.scrollEnabled = YES;  //计算高度大于最大高度,才可以滚动
        }else{
            self.scrollEnabled = NO;   //不需要滚动
            [self p_adjustFrameWithHeight:calcHeight];
        }

        if (CHECK_VALID_DELEGATE(self.qsDelegate, @selector(textView:textViewHeightChange:))) {
        
            [self.qsDelegate textView:self textViewHeightChange:calcHeight];
        }
    }
}

//根据高度增长方向控制frame
- (void)p_adjustFrameWithHeight:(CGFloat)height{

    CGRect originFrame = self.frame;
    originFrame.size = CGSizeMake(originFrame.size.width, height);

    if (self.growDirection == QSTextViewGrowDirectionUp) {
        //修改y
        originFrame.origin = CGPointMake(originFrame.origin.x, CGRectGetMaxY(self.frame) - height);
    }

    self.frame = originFrame;
    [self.superview layoutIfNeeded];
}

三、设置边框:无(默认)、虚线、实线

思路:定义一个CAShapeLayer实例borderLayer,添加到TextView上去,重写layoutSubviews,根据边框样式修改borderLayer的属性,充分利用贝塞尔曲线来为borderLayer绘制线条,如:(带圆角的)虚线、(带圆角的)实线等,当然我们也支持设置边框的颜色(borderColor)和宽度(borderWidth),圆角(cornerRadius)

//边框的枚举类型
 typedef NS_ENUM(NSInteger,QSTextViewBorderLineStyle) {

    QSTextViewBorderLineStyleNone = 0,   //无边框线
    QSTextViewBorderLineStyleSolid = 1, //实线
    QSTextViewBorderLineStyleDash = 2,   //虚线
    //... 可以继续扩展
};

//在layoutSubviews方法中添加边框的特性
- (void)layoutSubviews{

    self.borderLayer.hidden = (self.borderLineStyle == QSTextViewBorderLineStyleNone ? YES : NO);

    if (!self.borderLayer.hidden) {
    
        self.borderLayer.strokeColor = _borderColor.CGColor ? _borderColor.CGColor : [UIColor blackColor].CGColor;
        self.borderLayer.lineWidth = _borderWidth > 0 ? _borderWidth : 1;
    
        UIBezierPath *path = [UIBezierPath bezierPathWithRoundedRect:self.bounds cornerRadius:_cornerRadius];
        self.borderLayer.path = path.CGPath;
    }

    if (self.borderLineStyle == QSTextViewBorderLineStyleDash) {
    
        self.borderLayer.lineDashPattern = @[@5, @5];
    
    }else if(self.borderLineStyle == QSTextViewBorderLineStyleSolid){
    
        self.borderLayer.lineDashPattern = nil;
    }else{
        //定制其他边框线
    }
   [super layoutSubviews];
}

四、输入文本时候,自动识别url,并可以点击打开url

思路:利用NSDataDetector来实现URL的识别工作,实现touchesBegan方法来获取点击文本位置,从而获取点击的urlString。

/**
 识别URL
 */
- (void) detectorUrlPattern {

    NSDataDetector *dataDetector = [NSDataDetector dataDetectorWithTypes:NSTextCheckingTypeLink error:nil];
    NSArray *resultArray = [dataDetector matchesInString:self.textStorage.string options:NSMatchingReportProgress range:NSMakeRange(0, self.textStorage.string.length)];

    //清除
    [self.urlModels removeAllObjects];
    self.textColor = _originTextColor;

    for (NSTextCheckingResult *result in resultArray) {
    
        NSString *urlString = [self.textStorage.string substringWithRange:result.range];
        if ([urlString hasPrefix:@"http://"] || [urlString hasPrefix:@"https://"]) {
        
            QSTextViewUrlModel *urlModel = [[QSTextViewUrlModel alloc]initWithUrlString:urlString urlRange:result.range];
            [self.urlModels addObject:urlModel];
        
            NSMutableAttributedString *urlAttributedString = [[NSMutableAttributedString alloc]initWithString:urlString];
            [urlAttributedString addAttribute:NSForegroundColorAttributeName value:[UIColor blueColor] range:NSMakeRange(0, urlString.length)];
            [urlAttributedString addAttribute:NSFontAttributeName value:self.font range:NSMakeRange(0, urlString.length)];
            [self.textStorage replaceCharactersInRange:result.range withAttributedString:urlAttributedString];
        }
    }
}

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event{

    CGPoint point = [[touches anyObject] locationInView:self];
    [self.urlModels enumerateObjectsUsingBlock:^(QSTextViewUrlModel *urlModel, NSUInteger idx, BOOL * _Nonnull stop) {
    
        self.selectedRange = urlModel.urlRange;
        NSArray *selectionRects = [self selectionRectsForRange:self.selectedTextRange];
        for (UITextSelectionRect *textSelectionRect in selectionRects) {
            if (CGRectContainsPoint(textSelectionRect.rect, point)) {
                NSLog(@"click address%@",urlModel.urlString);
                if (CHECK_VALID_DELEGATE(self.qsDelegate, @selector(textView:openClickUrl:))) {
                    [self.qsDelegate textView:self openClickUrl:[NSURL URLWithString:urlModel.urlString]];
                }
                *stop = YES;
                break;
            }
        }
    }];
}

五、使用实例

- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view, typically from a nib.
    //....
    [self.view addSubview:self.textView];
     //....
}

- (QSTextView *)textView{

    if (!_textView) {
    
        _textView = [[QSTextView alloc]initWithFrame:CGRectMake(15, 150, SCREEN_WIDTH - 30, 36)];
        _textView.font = [UIFont systemFontOfSize:16];
        _textView.maxNumberOfLines = 2;
        _textView.backgroundColor = [UIColor clearColor];
        _textView.textColor = [UIColor blackColor];

        //占位文本
        _textView.placeholder = @"别憋着,说两句吧。";
        _textView.placeholderFont = [UIFont systemFontOfSize:15];
        _textView.placeholderColor = [UIColor greenColor];
    
        //高度增长方向
        _textView.growDirection = QSTextViewGrowDirectionUp;
    
        //边框
        _textView.borderLineStyle = QSTextViewBorderLineStyleDash;
        _textView.borderColor = [UIColor redColor];
        _textView.borderWidth = 2.0f;
        _textView.cornerRadius = 2.0f;
    
        //支持识别和点击url
        _textView.canDetectUrl = YES;
    
        //代理
        _textView.qsDelegate = self;
    }
    return _textView;
}
初始态.png
编辑中(未超出限定行数.png
编辑中(超出限定行数).png
  • textView中的链接是可以点击的。

源码直通车:QSUseTextViewDemo

篇外:新增TextView的这些特性,是出于需求,后面如果还有其他需求,继续增加。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,390评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,821评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,632评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,170评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,033评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,098评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,511评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,204评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,479评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,572评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,341评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,213评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,576评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,893评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,171评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,486评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,676评论 2 335

推荐阅读更多精彩内容

  • 发现 关注 消息 iOS 第三方库、插件、知名博客总结 作者大灰狼的小绵羊哥哥关注 2017.06.26 09:4...
    肇东周阅读 11,943评论 4 60
  • 不早不晚的下班时间,回到家,泡了面吃,洗了衣服。 阳台沙发,一盏烛光,音乐,和放空的心情。 晚上,《最后一公里》推...
    清楚明白阅读 234评论 0 0
  • 亲爱的,我就快到了 不知道从何时起,关于火车,我最多的去向就是你的所在地,合肥,上海,南京。不管几点的我都坐过,因...
    庸者的救赎阅读 701评论 8 5
  • 开学前跟闺蜜聚会,她跟我说她好慌,我问她慌什么,她说她今天去学吉他,然后她的老师问她什么时候分手啊,大学异地一...
    鬼栀阅读 483评论 0 1
  • 二年级的奖惩制度搞不起来,和小朋友商量,说取消这个制度,大家表示同意。要想一个新的制度真的好难哦。 今天给一年级的...
    栗子嘻嘻阅读 108评论 0 0