史上最好用的UITextview子类

gitDemo地址:https://github.com/laity1991/WKTextView

一.前言
  • 之前做项目封装textView时存在一些bug一直未解决:文本信息在用户粘贴过长超出设定的最大字数时、带emoji时截取显示半个或乱码(因为emoji在iOS中使用的是UTF16也就是占位符是8+8两个字节的,占的长度为2,因此在计算字数时一个表情就占了2)
  • 查了网上的一些封装demo,或是不太全或是存在一些小bug,在此我整理封装了一套WKTextView,方便大家使用和自己备用,主要解决问题:**
    1.添加了占位文本 ,类似于textField的placeholder  
    2.中,英文字符输入时限制。
    3.带emoji时截取显示半个或乱码字符处理。
    4.处理了用户在粘贴过来文本超出字数限制存在bug的情况**
  • ** 这里用到了我写的UIView的一个分类UIView+WKCategory (重写frame setter、getter方法),注释比较详细,不做过多赘述**
二 .WKTextView源码
  • *** WKTextView.h ***

    #import <UIKit/UIKit.h>
    @interface WKTextView : UITextView
    //文字
    @property(nonatomic,copy) NSString *myPlaceholder;
    //文字颜色
    @property(nonatomic,strong) UIColor *myPlaceholderColor;
    //最多输入字数
    @property (nonatomic, assign) NSInteger maxNum;
    ///右下角统计字数label
    @property (nonatomic, strong) UILabel *countLabel;
    @end
    
  • *** WKTextView.m ***
    #import "WKTextView.h"
    #import "UIView+WKCategory.h"
    @interface WKTextView()<UITextViewDelegate>
    @property (nonatomic,weak) UILabel *placeholderLabel;
    @end

    @implementation WKTextView
    
     - (instancetype)initWithFrame:(CGRect)frame{
      self = [super initWithFrame:frame];
      if (self) {
      UILabel *placeholderLabel = [[UILabel alloc]init];
      [self addSubview:placeholderLabel];
      placeholderLabel.textColor = [UIColor lightGrayColor];
      self.placeholderLabel= placeholderLabel;
      UILabel *countLabel = [[UILabel alloc]init];
      [self addSubview:countLabel];
      countLabel.frame = CGRectMake(self.width - 60, self.height - 40, 60, 40);
      countLabel.textAlignment = NSTextAlignmentCenter;
      countLabel.textColor = [UIColor lightGrayColor];
      countLabel.text = @"0/100";
      self.countLabel = countLabel;
      ///设置边框
      self.layer.masksToBounds = YES;
      self.layer.cornerRadius = 4;
      self.layer.borderColor = [UIColor grayColor].CGColor;
      self.layer.borderWidth = 1;
      
      self.delegate = self;
      return self;
    }
    - (void)layoutSubviews{
        [super layoutSubviews];
        self.placeholderLabel.x = 8;
        self.placeholderLabel.y = 8;
        self.placeholderLabel.width = self.width - 
        2*self.placeholderLabel.x;
        ///根据占位文字myPlaceholder 算出占位Label的高度(宽度已定 高速自适应)
        CGSize maxSize = CGSizeMake(self.placeholderLabel.width, MAXFLOAT);
        self.placeholderLabel.height = [self.myPlaceholder boundingRectWithSize:maxSize 
        options:NSStringDrawingUsesFontLeading | NSStringDrawingUsesLineFragmentOrigin attributes:@{NSFontAttributeName:self.placeholderLabel.font} context:nil].size.height;
    }
    
    ///箭头textView的输入文字变化 以此控制占位label的显示和隐藏
    - (void)textDidChange{
       //hasText是UITextView的属性 如果textView输入了文字就是
       YES 没有文字就是NO
        self.placeholderLabel.hidden = self.hasText;
    
    }
    
    #pragma mark - UITextViewDelegate
    
    - (BOOL)textView:(UITextView *)textView 
       shouldChangeTextInRange:(NSRange)range 
       replacementText:(NSString *)text{
       if ([text isEqualToString:@"\n"]) {
      [self resignFirstResponder];
      return NO;
     }
    UITextRange *selectedRange = [textView markedTextRange];
    //获取高亮部分
     UITextPosition *pos = [textView 
     positionFromPosition:selectedRange.start offset:0];
     //获取高亮部分内容
     //NSString * selectedtext = [textView 
     textInRange:selectedRange];
    
    //如果有高亮且当前字数开始位置小于最大限制时允许输入
      if (selectedRange && pos) {
      NSInteger startOffset = [textView offsetFromPosition:textView.beginningOfDocument toPosition:selectedRange.start];
      NSInteger endOffset = [textView offsetFromPosition:textView.beginningOfDocument toPosition:selectedRange.end];
      NSRange offsetRange = NSMakeRange(startOffset, endOffset - startOffset);
      
      if (offsetRange.location < self.maxNum) {
          return YES;
      }
      else
      {
          return NO;
      }
     }
       NSString *comcatstr = [textView.text stringByReplacingCharactersInRange:range withString:text];
    
       NSInteger caninputlen = self.maxNum - comcatstr.length;
    
       if (caninputlen >= 0)
       {
      return YES;
        }
        else
        {
      NSInteger len = text.length + caninputlen;
      //防止当text.length + caninputlen < 0时,使得rg.length为一个非法最大正数出错
      NSRange rg = {0,MAX(len,0)};
      
      if (rg.length > 0)
      {
          NSString *s = @"";
          //判断是否只普通的字符或asc码(对于中文和表情返回NO)
          BOOL asc = [text canBeConvertedToEncoding:NSASCIIStringEncoding];
          if (asc) {
              s = [text substringWithRange:rg];//因为是ascii码直接取就可以了不会错
                   }
          else
          {
              __block NSInteger idx = 0;
              __block NSString  *trimString = @"";//截取出的字串
              //使用字符串遍历,这个方法能准确知道每个emoji是占一个unicode还是两个
              [text enumerateSubstringsInRange:NSMakeRange(0, [text length])
                                       options:NSStringEnumerationByComposedCharacterSequences
                                    usingBlock: ^(NSString* substring, NSRange substringRange, NSRange enclosingRange, BOOL* stop) {
                                        
                                        if (idx >= rg.length) {
                                            *stop = YES; //取出所需要就break,提高效率
                                            return ;
                                        }
                                        
                                        trimString = [trimString stringByAppendingString:substring];
                                        
                                        idx++;
                                    }];
              
              s = trimString;
          }
          //rang是指从当前光标处进行替换处理(注意如果执行此句后面返回的是YES会触发didchange事件)
          [textView setText:[textView.text stringByReplacingCharactersInRange:range withString:s]];
          //既然是超出部分截取了,哪一定是最大限制了。
          self.countLabel.text = [NSString stringWithFormat:@"%ld/%ld",(long)self.maxNum,(long)self.maxNum];
          
      }
      return NO;
       }
      }
    
      - (void)textViewDidChange:(UITextView *)textView{
        self.placeholderLabel.hidden = self.hasText;
         UITextRange *selectedRange = [textView markedTextRange];
        //获取高亮部分
        UITextPosition *pos = [textView positionFromPosition:selectedRange.start offset:0];
    
       //如果在变化中是高亮部分在变,就不要计算字符了
       if (selectedRange && pos) {
      return;
       }
    
       NSString  *nsTextContent = textView.text;
       NSInteger existTextNum = nsTextContent.length;
    
       if (existTextNum > self.maxNum)
       {
      //截取到最大位置的字符(由于超出截部分在should时被处理了所在这里这了提高效率不再判断)
      NSString *s = [nsTextContent substringToIndex:self.maxNum];
      
      [textView setText:s];
      }
      if (existTextNum > 100) {
       existTextNum = 100;
        }
       //不让显示负数
     self.countLabel.text = [NSString 
     stringWithFormat:@"%ld/%ld",MAX(0, existTextNum),
     (long)self.maxNum
                          ];
    
     }
     #pragma mark - setter
    
    - (void)setMaxNum:(NSInteger)maxNum{
       _maxNum = maxNum;
        self.countLabel.text = [NSString stringWithFormat:@"0/%ld",
       (long)_maxNum];
      }
    
    - (void)setMyPlaceholder:(NSString *)myPlaceholder{
          _myPlaceholder = myPlaceholder;
          self.placeholderLabel.text = _myPlaceholder;
          ///重新计算占位label frame
          [self setNeedsLayout];
        }
    
    - (void)setMyPlaceholderColor:(UIColor *)myPlaceholderColor{
              _myPlaceholderColor = myPlaceholderColor;
              self.countLabel.textColor = myPlaceholderColor;
              self.placeholderLabel.textColor = _myPlaceholderColor;
          }
    
      ///从写TextView setFont方法 使占位labe、TextView、文字统计
        Label Font一致
    - (void)setFont:(UIFont *)font{
              [super setFont:font];
              self.placeholderLabel.font = font;
              self.countLabel.font = font;
              ///重新计算占位label frame
              [self setNeedsLayout];
         }
        @end
    
  • *** Demo ***

       - (void)prepareForUI{
                ///添加到父控件
                WKTextView *textView = [[WKTextView alloc]initWithFrame:CGRectMake(50, 50, self.view.width - 100, 200)];
                ///设置文本输入框的占位字符
                textView.myPlaceholder = @"我是占位字符串...";
                textView.font = [UIFont systemFontOfSize:14];
                textView.maxNum = 100;
                [self.view addSubview:textView];
            }
    
  • 相关属性都暴露了出来,可以自行设置,效果如下:

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

推荐阅读更多精彩内容