如何优雅的实现自适应高度的UITextView

作者: StrivEver | 来源:发表于2017-12-28 17:25 被阅读1103次

    之前写过一个自适应高度的textView,用起来好像不是很优雅。于是使用的category重新写了一个,完全无侵入,使用起来很方便,高效。

    一.首先看一下文件目录
    文件目录
    二.看一下实现了那些功能
    • 根据内容自适应高度,支持最大高度,最小高度的设置。
    • 完美实现自定义占位符,无侵入
    • 任意设置行间距
    • 支持手写frame以及xib autoLayout
    • 支持xib设置生效
    二.说一下实现思路
      1. 我们知道UITextView 继承与UIScrollView,那么我们是不是可以在系统设置UITextView 的contentSize的时候重新设置textView的高度呢,这样的话我们只需要hook一下UIScrollView的setContentSize:这个函数。这样既可以便捷快速的让textView去自适应高度。   
      2. 占位图的实现 我们在textView上添加了一个完全一模一杨的textView,这样就完美避免了重新设置textContainerInset之后 占位符位置不准确的问题。监听文字变化的时候,使用了通知,进行无侵入监听,不建议使用代理的方式进行监听文字变化。   
      3. 设置行间距重新设置TextView的 typingAttributes属性        
    
    三.代码具体实现

    首先看一下头文件"UITextView+STAutoHeight.h":

    static NSString * const st_layout_frame = @"st_layout_frame";
    static NSString * const st_auto_layout = @"st_auto_layout";
    IB_DESIGNABLE
    @interface UITextView (STAutoHeight)
    
    /**
     是否自适应高度
     */
    
    @property (nonatomic, assign)IBInspectable BOOL isAutoHeightEnable;
    
    /**
     设置最大高度
     */
    @property (nonatomic, assign)IBInspectable CGFloat st_maxHeight;
    
    /**
     最小高度
     */
    @property (nonatomic, assign) CGFloat st_minHeight;
    
    /**
     占位符
     */
    @property (nonatomic, copy)IBInspectable NSString * st_placeHolder;
    /**
     占位符颜色
     */
    @property (nonatomic, strong) UIColor * st_placeHolderColor;
    
    /**
     占位Label
     */
    @property (nonatomic, strong) UITextView * st_placeHolderLabel;
    
    /**
     行间距
     */
    @property (nonatomic, assign)IBInspectable CGFloat st_lineSpacing;
    @property (nonatomic, strong) NSLayoutConstraint *heightConstraint;
    @property (nonatomic, copy) NSString *layout_key;
    @property (nonatomic, copy) void(^textViewHeightDidChangedHandle)(CGFloat textViewHeight);
    @end
    
    • 高度自适应
      UIScrollView+STAutoHeight.h中hook setContentSize:函数
    + (void)load{
        static dispatch_once_t onceToken;
        dispatch_once(&onceToken, ^{
            method_exchangeImplementations(class_getInstanceMethod(self, @selector(setContentSize:)), class_getInstanceMethod(self, @selector(st_setContentSize:)));
        });
    }
    

    实现st_setContentSize:

    - (void)st_setContentSize:(CGSize)contentSize{
        if ([self isKindOfClass:[UITextView class]]) {
            UITextView * t_view = (UITextView *)self;
            if (t_view.isAutoHeightEnable) {
                NSString * key = t_view.layout_key;
                CGFloat height = contentSize.height;
                CGRect frame = self.frame;
                if (t_view.st_maxHeight > 0 && height > t_view.st_maxHeight) {
                    height = t_view.st_maxHeight;
                }
                if (height < t_view.st_minHeight && t_view.st_minHeight > 0) {
                    height = t_view.st_minHeight;
                }
                frame.size.height = height;
                if ([key isEqualToString:st_layout_frame]) {
                    self.frame = frame;
                }else{
                    if (t_view.heightConstraint) {
                        if ([t_view.heightConstraint isKindOfClass:NSClassFromString(@"NSContentSizeLayoutConstraint")]) {
                            self.scrollEnabled = NO;
                        }else{
                            //主动添加了高度约束
                            self.scrollEnabled = YES;
                            self.frame = frame;
                            t_view.heightConstraint.constant = height;
                        }
                    }else{
                        self.scrollEnabled = NO;
                    }
                   
                }
            }
        }
         [self st_setContentSize:contentSize];
    }
    

    st_setcontentSize函数中 主要是判断是不是frame布局,frame布局的话直接改变高度即可。

    如果是xib布局的话,先检查一下是否设置了高度约束,heightConstraint属性用来获取设置的高度约束,如果为nil,说明没有设置高度约束,我们需要设置textView self.scrollEnabled = NO这样就可以自适应高度了。 需要注意的是当我们没有设置 textView约束的时候,系统默认设置了textView的抗压缩属性 (NSContentSizeLayoutConstraint)默认的优先级为750,这就解释了为什么women设置self.scrollEnabled =NO之后为什么会自适应高度了。如果使我们自己设置的heightConstraint的话 我们改变这个约束即可 t_view.heightConstraint.constant = height;但是会出现界面抖动,我这里暂时设置了self.frame = frame;解决了这个问题

    xib中可直接设置属性


    xib直接设置属性

    原理其实很简单

    #import <UIKit/UIKit.h>
    static NSString * const st_layout_frame = @"st_layout_frame";
    static NSString * const st_auto_layout = @"st_auto_layout";
    IB_DESIGNABLE
    @interface UITextView (STAutoHeight)
    
    /**
     是否自适应高度
     */
    
    @property (nonatomic, assign)IBInspectable BOOL isAutoHeightEnable;
    
    /**
     设置最大高度
     */
    @property (nonatomic, assign)IBInspectable CGFloat st_maxHeight;
    

    关键字 IB_DESIGNABLE 在头文件申明,在需要设置属性前 加关键字 IBInspectable

    • 占位符实现

      UITextView+STAutoHeight.h 中动态添加了一个 textView,用来充当占位符,代码如下

    - (UITextView *)st_placeHolderLabel{
        UITextView * placeHolderLabel = objc_getAssociatedObject(self, _cmd);
        if (!placeHolderLabel) {
            placeHolderLabel = [[UITextView alloc]initWithFrame:self.bounds];
            placeHolderLabel.textContainerInset = self.textContainerInset;
            placeHolderLabel.font = self.font;
            placeHolderLabel.userInteractionEnabled = NO;
            placeHolderLabel.backgroundColor = [UIColor clearColor];
            placeHolderLabel.textColor = self.st_placeHolderColor;
            placeHolderLabel.scrollEnabled = NO;
            [self addSubview:placeHolderLabel];
            objc_setAssociatedObject(self, _cmd, placeHolderLabel, OBJC_ASSOCIATION_RETAIN);
        }
        return placeHolderLabel;
    }
    - (void)setSt_placeHolderLabel:(UILabel *)st_placeHolderLabel{
        objc_setAssociatedObject(self, @selector(st_placeHolderLabel), st_placeHolderLabel, OBJC_ASSOCIATION_RETAIN);
    }
    

    需要注意的是监听textView的文字变化,我用的是系统通知,看代码:

    - (void)setSt_placeHolder:(NSString *)st_placeHolder{
        if (!st_placeHolder) {
            return;
        }
        if(!self.st_observer){
            __weak typeof(self) weakSelf = self;
           id observer = [[NSNotificationCenter defaultCenter]addObserverForName:UITextViewTextDidChangeNotification object:nil queue:[NSOperationQueue mainQueue] usingBlock:^(NSNotification * _Nonnull note) {
                __strong typeof(weakSelf) strongSelf = weakSelf;
                if (note.object == strongSelf) {
                    if (strongSelf.text.length == 0) {
                        strongSelf.st_placeHolderLabel.hidden = NO;
                    }else{
                        strongSelf.st_placeHolderLabel.hidden = YES;
                    }
                }
            }];
            self.st_observer = observer;
        }
        objc_setAssociatedObject(self, @selector(st_placeHolder), st_placeHolder, OBJC_ASSOCIATION_COPY);
        self.st_placeHolderLabel.text = st_placeHolder;
    }
    

    实用block方式的通知,在block一定要弱引用,否则会造成循环引用,切记。
    这种方式注销通知也比较特殊,我用了一个@property (nonatomic, weak) id st_observer;全局变量去接收这个对象,在dealloc时候注销通知[[NSNotificationCenter defaultCenter]removeObserver:self.st_observer];,(关键字不用weak,需要在注销通知后手动 self.st_observer = nil

    • 改变行间距 其实很简单 看代码
    - (void)setSt_lineSpacing:(CGFloat)st_lineSpacing{
        objc_setAssociatedObject(self, @selector(st_lineSpacing), @(st_lineSpacing), OBJC_ASSOCIATION_ASSIGN);
        
        NSMutableParagraphStyle *paragraphStyle = [NSMutableParagraphStyle new];
        paragraphStyle.lineSpacing = self.st_lineSpacing;// 字体的行间距
        NSMutableDictionary * attributes = self.typingAttributes.mutableCopy;
        [attributes setValue:paragraphStyle forKey:NSParagraphStyleAttributeName];
        self.typingAttributes = attributes;
        if (self.text.length > 0) {
            self.text = self.text;
        }
    }
    

    这里NSMutableDictionary * attributes = self.typingAttributes.mutableCopy;需要将默认的属性copy过来,需要注意。

    4.看一下实际效果图吧
    • frame布局的使用
     _t_view1 = [[UITextView alloc]initWithFrame:CGRectMake(15, 84, self.view.bounds.size.width - 30, 40)];
        _t_view1.isAutoHeightEnable = YES;
        _t_view1.font = [UIFont systemFontOfSize:15];
        _t_view1.text = @"测试一下我是自适应高度的TextView";
        _t_view1.st_placeHolder = @"请输入您的信息";
        _t_view1.st_maxHeight = 200;
        _t_view1.layer.borderWidth = 1;
        _t_view1.layer.borderColor = [UIColor lightGrayColor].CGColor;
        _t_view1.backgroundColor = [UIColor whiteColor];
        _t_view1.st_lineSpacing = 5;
        _t_view1.textViewHeightDidChangedHandle = ^(CGFloat textViewHeight) {
            
        };
        [self.view addSubview:_t_view1];
    
    使用frame效果图.gif
    • 在xib中设置相关属性,打开高度自适应,使用占位符,改变行间距,设置如下图:


      xib设置属性
      看一下运行效果 xib设置效果图.gif
    好了在这里demo 在这里,觉着有用的话star一下,希望能够帮到大家吧。

    相关文章

      网友评论

        本文标题:如何优雅的实现自适应高度的UITextView

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