如何优雅的实现自适应高度的UITextView
之前写过一个自适应高度的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
中hooksetContentSize:
函数
+ (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
,说明没有设置高度约束,我们需要设置textViewself.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