New file |
| | |
| | | // |
| | | // UITextView+WZB.m |
| | | // WZBTextView-demo |
| | | // |
| | | // Created by normal on 2016/11/14. |
| | | // Copyright © 2016年 WZB. All rights reserved. |
| | | // |
| | | |
| | | #import "UITextView+WZB.h" |
| | | #import <objc/runtime.h> |
| | | |
| | | // 占位文字 |
| | | static const void *WZBPlaceholderViewKey = &WZBPlaceholderViewKey; |
| | | // 占位文字颜色 |
| | | static const void *WZBPlaceholderColorKey = &WZBPlaceholderColorKey; |
| | | // 最大高度 |
| | | static const void *WZBTextViewMaxHeightKey = &WZBTextViewMaxHeightKey; |
| | | // 最小高度 |
| | | static const void *WZBTextViewMinHeightKey = &WZBTextViewMinHeightKey; |
| | | // 高度变化的block |
| | | static const void *WZBTextViewHeightDidChangedBlockKey = &WZBTextViewHeightDidChangedBlockKey; |
| | | // 存储添加的图片 |
| | | static const void *WZBTextViewImageArrayKey = &WZBTextViewImageArrayKey; |
| | | // 存储最后一次改变高度后的值 |
| | | static const void *WZBTextViewLastHeightKey = &WZBTextViewLastHeightKey; |
| | | |
| | | @interface UITextView () |
| | | |
| | | // 存储添加的图片 |
| | | @property (nonatomic, strong) NSMutableArray *wzb_imageArray; |
| | | // 存储最后一次改变高度后的值 |
| | | @property (nonatomic, assign) CGFloat lastHeight; |
| | | |
| | | @end |
| | | |
| | | @implementation UITextView (WZB) |
| | | |
| | | #pragma mark - Swizzle Dealloc |
| | | + (void)load { |
| | | // 交换dealoc |
| | | Method dealoc = class_getInstanceMethod(self.class, NSSelectorFromString(@"dealloc")); |
| | | Method myDealloc = class_getInstanceMethod(self.class, @selector(myDealloc)); |
| | | method_exchangeImplementations(dealoc, myDealloc); |
| | | } |
| | | |
| | | - (void)myDealloc { |
| | | // 移除监听 |
| | | [[NSNotificationCenter defaultCenter] removeObserver:self]; |
| | | |
| | | UITextView *placeholderView = objc_getAssociatedObject(self, WZBPlaceholderViewKey); |
| | | |
| | | // 如果有值才去调用,这步很重要 |
| | | if (placeholderView) { |
| | | NSArray *propertys = @[@"frame", @"bounds", @"font", @"text", @"textAlignment", @"textContainerInset"]; |
| | | for (NSString *property in propertys) { |
| | | @try { |
| | | [self removeObserver:self forKeyPath:property]; |
| | | } @catch (NSException *exception) {} |
| | | } |
| | | } |
| | | [self myDealloc]; |
| | | } |
| | | |
| | | #pragma mark - set && get |
| | | - (UITextView *)wzb_placeholderView { |
| | | |
| | | // 为了让占位文字和textView的实际文字位置能够完全一致,这里用UITextView |
| | | UITextView *placeholderView = objc_getAssociatedObject(self, WZBPlaceholderViewKey); |
| | | |
| | | if (!placeholderView) { |
| | | |
| | | // 初始化数组 |
| | | self.wzb_imageArray = [NSMutableArray array]; |
| | | |
| | | placeholderView = [[UITextView alloc] init]; |
| | | // 动态添加属性的本质是: 让对象的某个属性与值产生关联 |
| | | objc_setAssociatedObject(self, WZBPlaceholderViewKey, placeholderView, OBJC_ASSOCIATION_RETAIN_NONATOMIC); |
| | | placeholderView = placeholderView; |
| | | |
| | | // 设置基本属性 |
| | | placeholderView.scrollEnabled = placeholderView.userInteractionEnabled = NO; |
| | | // self.scrollEnabled = placeholderView.scrollEnabled = placeholderView.showsHorizontalScrollIndicator = placeholderView.showsVerticalScrollIndicator = placeholderView.userInteractionEnabled = NO; |
| | | placeholderView.textColor = [UIColor lightGrayColor]; |
| | | placeholderView.backgroundColor = [UIColor clearColor]; |
| | | [self refreshPlaceholderView]; |
| | | [self addSubview:placeholderView]; |
| | | |
| | | // 监听文字改变 |
| | | [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(textViewTextChange) name:UITextViewTextDidChangeNotification object:self]; |
| | | |
| | | // 这些属性改变时,都要作出一定的改变,尽管已经监听了TextDidChange的通知,也要监听text属性,因为通知监听不到setText: |
| | | NSArray *propertys = @[@"frame", @"bounds", @"font", @"text", @"textAlignment", @"textContainerInset"]; |
| | | |
| | | // 监听属性 |
| | | for (NSString *property in propertys) { |
| | | [self addObserver:self forKeyPath:property options:NSKeyValueObservingOptionNew context:nil]; |
| | | } |
| | | |
| | | } |
| | | return placeholderView; |
| | | } |
| | | |
| | | - (void)setWzb_placeholder:(NSString *)placeholder |
| | | { |
| | | // 为placeholder赋值 |
| | | [self wzb_placeholderView].text = placeholder; |
| | | } |
| | | |
| | | - (NSString *)wzb_placeholder |
| | | { |
| | | // 如果有placeholder值才去调用,这步很重要 |
| | | if (self.placeholderExist) { |
| | | return [self wzb_placeholderView].text; |
| | | } |
| | | return nil; |
| | | } |
| | | |
| | | - (void)setWzb_placeholderColor:(UIColor *)wzb_placeholderColor |
| | | { |
| | | // 如果有placeholder值才去调用,这步很重要 |
| | | if (!self.placeholderExist) { |
| | | NSLog(@"请先设置placeholder值!"); |
| | | } else { |
| | | self.wzb_placeholderView.textColor = wzb_placeholderColor; |
| | | |
| | | // 动态添加属性的本质是: 让对象的某个属性与值产生关联 |
| | | objc_setAssociatedObject(self, WZBPlaceholderColorKey, wzb_placeholderColor, OBJC_ASSOCIATION_RETAIN_NONATOMIC); |
| | | } |
| | | } |
| | | |
| | | - (UIColor *)wzb_placeholderColor |
| | | { |
| | | return objc_getAssociatedObject(self, WZBPlaceholderColorKey); |
| | | } |
| | | |
| | | - (void)setWzb_maxHeight:(CGFloat)wzb_maxHeight |
| | | { |
| | | CGFloat max = wzb_maxHeight; |
| | | |
| | | // 如果传入的最大高度小于textView本身的高度,则让最大高度等于本身高度 |
| | | if (wzb_maxHeight < self.frame.size.height) { |
| | | max = self.frame.size.height; |
| | | } |
| | | |
| | | objc_setAssociatedObject(self, WZBTextViewMaxHeightKey, [NSString stringWithFormat:@"%lf", max], OBJC_ASSOCIATION_COPY_NONATOMIC); |
| | | } |
| | | |
| | | - (CGFloat)wzb_maxHeight |
| | | { |
| | | return [objc_getAssociatedObject(self, WZBTextViewMaxHeightKey) doubleValue]; |
| | | } |
| | | |
| | | - (void)setWzb_minHeight:(CGFloat)wzb_minHeight |
| | | { |
| | | objc_setAssociatedObject(self, WZBTextViewMinHeightKey, [NSString stringWithFormat:@"%lf", wzb_minHeight], OBJC_ASSOCIATION_COPY_NONATOMIC); |
| | | } |
| | | |
| | | - (CGFloat)wzb_minHeight |
| | | { |
| | | return [objc_getAssociatedObject(self, WZBTextViewMinHeightKey) doubleValue]; |
| | | } |
| | | |
| | | - (void)setWzb_textViewHeightDidChanged:(textViewHeightDidChangedBlock)wzb_textViewHeightDidChanged |
| | | { |
| | | objc_setAssociatedObject(self, WZBTextViewHeightDidChangedBlockKey, wzb_textViewHeightDidChanged, OBJC_ASSOCIATION_COPY_NONATOMIC); |
| | | } |
| | | |
| | | - (textViewHeightDidChangedBlock)wzb_textViewHeightDidChanged |
| | | { |
| | | void(^textViewHeightDidChanged)(CGFloat currentHeight) = objc_getAssociatedObject(self, WZBTextViewHeightDidChangedBlockKey); |
| | | return textViewHeightDidChanged; |
| | | } |
| | | |
| | | - (NSArray *)wzb_getImages |
| | | { |
| | | return self.wzb_imageArray; |
| | | } |
| | | |
| | | - (void)setLastHeight:(CGFloat)lastHeight { |
| | | objc_setAssociatedObject(self, WZBTextViewLastHeightKey, [NSString stringWithFormat:@"%lf", lastHeight], OBJC_ASSOCIATION_COPY_NONATOMIC); |
| | | } |
| | | |
| | | - (CGFloat)lastHeight { |
| | | return [objc_getAssociatedObject(self, WZBTextViewLastHeightKey) doubleValue]; |
| | | } |
| | | |
| | | - (void)setWzb_imageArray:(NSMutableArray *)wzb_imageArray { |
| | | objc_setAssociatedObject(self, WZBTextViewImageArrayKey, wzb_imageArray, OBJC_ASSOCIATION_RETAIN_NONATOMIC); |
| | | } |
| | | |
| | | - (NSMutableArray *)wzb_imageArray { |
| | | return objc_getAssociatedObject(self, WZBTextViewImageArrayKey); |
| | | } |
| | | |
| | | - (void)wzb_autoHeightWithMaxHeight:(CGFloat)maxHeight |
| | | { |
| | | [self wzb_autoHeightWithMaxHeight:maxHeight textViewHeightDidChanged:nil]; |
| | | } |
| | | // 是否启用自动高度,默认为NO |
| | | static bool autoHeight = NO; |
| | | - (void)wzb_autoHeightWithMaxHeight:(CGFloat)maxHeight textViewHeightDidChanged:(textViewHeightDidChangedBlock)textViewHeightDidChanged |
| | | { |
| | | autoHeight = YES; |
| | | [self wzb_placeholderView]; |
| | | self.wzb_maxHeight = maxHeight; |
| | | if (textViewHeightDidChanged) self.wzb_textViewHeightDidChanged = textViewHeightDidChanged; |
| | | } |
| | | |
| | | #pragma mark - addImage |
| | | /* 添加一张图片 */ |
| | | - (void)wzb_addImage:(UIImage *)image |
| | | { |
| | | [self wzb_addImage:image size:CGSizeZero]; |
| | | } |
| | | |
| | | /* 添加一张图片 image:要添加的图片 size:图片大小 */ |
| | | - (void)wzb_addImage:(UIImage *)image size:(CGSize)size |
| | | { |
| | | [self wzb_insertImage:image size:size index:self.attributedText.length > 0 ? self.attributedText.length : 0]; |
| | | } |
| | | |
| | | /* 插入一张图片 image:要添加的图片 size:图片大小 index:插入的位置 */ |
| | | - (void)wzb_insertImage:(UIImage *)image size:(CGSize)size index:(NSInteger)index |
| | | { |
| | | [self wzb_addImage:image size:size index:index multiple:-1]; |
| | | } |
| | | |
| | | /* 添加一张图片 image:要添加的图片 multiple:放大/缩小的倍数 */ |
| | | - (void)wzb_addImage:(UIImage *)image multiple:(CGFloat)multiple |
| | | { |
| | | [self wzb_addImage:image size:CGSizeZero index:self.attributedText.length > 0 ? self.attributedText.length : 0 multiple:multiple]; |
| | | } |
| | | |
| | | /* 插入一张图片 image:要添加的图片 multiple:放大/缩小的倍数 index:插入的位置 */ |
| | | - (void)wzb_insertImage:(UIImage *)image multiple:(CGFloat)multiple index:(NSInteger)index |
| | | { |
| | | [self wzb_addImage:image size:CGSizeZero index:index multiple:multiple]; |
| | | } |
| | | |
| | | /* 插入一张图片 image:要添加的图片 size:图片大小 index:插入的位置 multiple:放大/缩小的倍数 */ |
| | | - (void)wzb_addImage:(UIImage *)image size:(CGSize)size index:(NSInteger)index multiple:(CGFloat)multiple { |
| | | if (image) [self.wzb_imageArray addObject:image]; |
| | | NSMutableAttributedString *attributedString = [[NSMutableAttributedString alloc] initWithAttributedString:self.attributedText]; |
| | | NSTextAttachment *textAttachment = [[NSTextAttachment alloc] init]; |
| | | textAttachment.image = image; |
| | | CGRect bounds = textAttachment.bounds; |
| | | if (!CGSizeEqualToSize(size, CGSizeZero)) { |
| | | bounds.size = size; |
| | | textAttachment.bounds = bounds; |
| | | } else if (multiple <= 0) { |
| | | CGFloat oldWidth = textAttachment.image.size.width; |
| | | CGFloat scaleFactor = oldWidth / (self.frame.size.width - 10); |
| | | textAttachment.image = [UIImage imageWithCGImage:textAttachment.image.CGImage scale:scaleFactor orientation:UIImageOrientationUp]; |
| | | } else { |
| | | bounds.size = image.size; |
| | | textAttachment.bounds = bounds; |
| | | } |
| | | |
| | | NSAttributedString *attrStringWithImage = [NSAttributedString attributedStringWithAttachment:textAttachment]; |
| | | [attributedString replaceCharactersInRange:NSMakeRange(index, 0) withAttributedString:attrStringWithImage]; |
| | | self.attributedText = attributedString; |
| | | [self textViewTextChange]; |
| | | [self refreshPlaceholderView]; |
| | | } |
| | | |
| | | |
| | | #pragma mark - KVO监听属性改变 |
| | | - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context { |
| | | [self refreshPlaceholderView]; |
| | | if ([keyPath isEqualToString:@"text"]) [self textViewTextChange]; |
| | | } |
| | | |
| | | // 刷新PlaceholderView |
| | | - (void)refreshPlaceholderView { |
| | | |
| | | UITextView *placeholderView = objc_getAssociatedObject(self, WZBPlaceholderViewKey); |
| | | |
| | | // 如果有值才去调用,这步很重要 |
| | | if (placeholderView) { |
| | | self.wzb_placeholderView.frame = self.bounds; |
| | | if (self.wzb_maxHeight < self.bounds.size.height) self.wzb_maxHeight = self.bounds.size.height; |
| | | self.wzb_placeholderView.font = self.font; |
| | | self.wzb_placeholderView.textAlignment = self.textAlignment; |
| | | self.wzb_placeholderView.textContainerInset = self.textContainerInset; |
| | | self.wzb_placeholderView.hidden = (self.text.length > 0 && self.text); |
| | | } |
| | | } |
| | | |
| | | // 处理文字改变 |
| | | - (void)textViewTextChange { |
| | | UITextView *placeholderView = objc_getAssociatedObject(self, WZBPlaceholderViewKey); |
| | | |
| | | // 如果有值才去调用,这步很重要 |
| | | if (placeholderView) { |
| | | self.wzb_placeholderView.hidden = (self.text.length > 0 && self.text); |
| | | } |
| | | // 如果没有启用自动高度,不执行以下方法 |
| | | if (!autoHeight) return; |
| | | if (self.wzb_maxHeight >= self.bounds.size.height) { |
| | | |
| | | // 计算高度 |
| | | NSInteger currentHeight = ceil([self sizeThatFits:CGSizeMake(self.bounds.size.width, MAXFLOAT)].height); |
| | | |
| | | // 如果高度有变化,调用block |
| | | if (currentHeight != self.lastHeight) { |
| | | // 是否可以滚动 |
| | | self.scrollEnabled = currentHeight >= self.wzb_maxHeight; |
| | | CGFloat currentTextViewHeight = currentHeight >= self.wzb_maxHeight ? self.wzb_maxHeight : currentHeight; |
| | | // 改变textView的高度 |
| | | if (currentTextViewHeight >= self.wzb_minHeight) { |
| | | CGRect frame = self.frame; |
| | | frame.size.height = currentTextViewHeight; |
| | | self.frame = frame; |
| | | // 调用block |
| | | if (self.wzb_textViewHeightDidChanged) self.wzb_textViewHeightDidChanged(currentTextViewHeight); |
| | | // 记录当前高度 |
| | | self.lastHeight = currentTextViewHeight; |
| | | } |
| | | } |
| | | } |
| | | |
| | | if (!self.isFirstResponder) [self becomeFirstResponder]; |
| | | } |
| | | |
| | | // 判断是否有placeholder值,这步很重要 |
| | | - (BOOL)placeholderExist { |
| | | |
| | | // 获取对应属性的值 |
| | | UITextView *placeholderView = objc_getAssociatedObject(self, WZBPlaceholderViewKey); |
| | | |
| | | // 如果有placeholder值 |
| | | if (placeholderView) return YES; |
| | | |
| | | return NO; |
| | | } |
| | | |
| | | @end |