YMTextView.m 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364
  1. //
  2. // YMTextView.m
  3. // andaowei
  4. //
  5. // Created by YoMi on 2023/11/6.
  6. //
  7. #import "YMTextView.h"
  8. @interface YMTextView () <UITextViewDelegate>
  9. @property (nonatomic, copy) YMTextViewHandler beginEditingHandler; ///< 文本开始编辑Block
  10. @property (nonatomic, copy) YMTextViewHandler changeHandler; ///< 文本改变Block
  11. @property (nonatomic, copy) YMTextViewHandler endEditingHandler; ///< 文本结束编辑Block
  12. @property (nonatomic, copy) YMTextViewHandler maxHandler; ///< 达到最大限制字符数Block
  13. @property (nonatomic, strong) UITextView *placeholderTextView; ///< placeholderTextView
  14. @property (nonatomic, strong) NSMutableArray *placeholderTextViewConstraints; ///<placeholderLabel的约束
  15. @property (nonatomic, assign) CGFloat lastHeight;///< 存储最后一次改变高度后的值
  16. @property (nonatomic, assign) BOOL needTextViewHeightChanged; ///<需要计算一次TextViewHeight
  17. @end
  18. @implementation YMTextView
  19. #pragma mark - Override
  20. - (void)dealloc
  21. {
  22. [[NSNotificationCenter defaultCenter] removeObserver:self];
  23. _beginEditingHandler = NULL;
  24. _changeHandler = NULL;
  25. _endEditingHandler = NULL;
  26. _maxHandler = NULL;
  27. }
  28. - (instancetype)initWithCoder:(NSCoder *)aDecoder
  29. {
  30. if (!(self = [super initWithCoder:aDecoder])) return nil;
  31. if ([[[UIDevice currentDevice] systemVersion] compare:@"10.0" options:NSNumericSearch] != NSOrderedAscending) {
  32. [self layoutIfNeeded];
  33. }
  34. [self initialize];
  35. return self;
  36. }
  37. - (instancetype)initWithFrame:(CGRect)frame
  38. {
  39. if (!(self = [super initWithFrame:frame])) return nil;
  40. [self initialize];
  41. return self;
  42. }
  43. - (BOOL)becomeFirstResponder
  44. {
  45. // 成为第一响应者时注册通知监听文本变化
  46. [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(textDidBeginEditing:) name:UITextViewTextDidBeginEditingNotification object:nil];
  47. [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(textDidChange:) name:UITextViewTextDidChangeNotification object:nil];
  48. [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(textDidEndEditing:) name:UITextViewTextDidEndEditingNotification object:nil];
  49. BOOL become = [super becomeFirstResponder];
  50. return become;
  51. }
  52. - (BOOL)resignFirstResponder
  53. {
  54. BOOL resign = [super resignFirstResponder];
  55. // 注销第一响应者时移除文本变化的通知, 以免影响其它的`UITextView`对象.
  56. [[NSNotificationCenter defaultCenter] removeObserver:self name:UITextViewTextDidBeginEditingNotification object:nil];
  57. [[NSNotificationCenter defaultCenter] removeObserver:self name:UITextViewTextDidChangeNotification object:nil];
  58. [[NSNotificationCenter defaultCenter] removeObserver:self name:UITextViewTextDidEndEditingNotification object:nil];
  59. return resign;
  60. }
  61. - (void)layoutSubviews {
  62. [super layoutSubviews];
  63. self.placeholderTextView.frame = CGRectMake(0, 0, self.frame.size.width, self.frame.size.height);
  64. if (self.needTextViewHeightChanged) {
  65. self.needTextViewHeightChanged = NO;
  66. [self textViewHeightDidChangedCalculate];
  67. }
  68. }
  69. #pragma mark - UITextViewDelegate
  70. - (BOOL)textViewShouldBeginEditing:(UITextView *)textView {
  71. if (textView == self.placeholderTextView) {
  72. [self becomeFirstResponder];
  73. return NO;
  74. }
  75. return YES;
  76. }
  77. #pragma mark - Private
  78. - (void)initialize
  79. {
  80. _disableFirstWhitespace = YES;
  81. _ym_textContainerInset = UIEdgeInsetsMake(8, 3, 8, 3);
  82. if (_maxLength == 0 || _maxLength == NSNotFound) {
  83. _maxLength = NSUIntegerMax;
  84. }
  85. if (!_placeholderColor) {
  86. _placeholderColor = [UIColor colorWithRed:0.780 green:0.780 blue:0.804 alpha:1.000];
  87. }
  88. // 基本设定 (需判断是否在Storyboard中设置了值)
  89. if (!self.backgroundColor) {
  90. self.backgroundColor = [UIColor whiteColor];
  91. }
  92. if (!self.font) {
  93. self.font = [UIFont systemFontOfSize:15.f];
  94. }
  95. // placeholderTextView
  96. self.placeholderTextView = [UITextView new];
  97. self.placeholderTextView.font = self.font;
  98. self.placeholderTextView.text = _placeholder;
  99. self.placeholderTextView.textColor = _placeholderColor;
  100. self.placeholderTextView.delegate = self;
  101. self.placeholderTextView.backgroundColor = [UIColor clearColor];
  102. self.placeholderTextView.textContainer.lineBreakMode = NSLineBreakByTruncatingTail;
  103. [self addSubview:self.placeholderTextView];
  104. self.ym_textContainerInset = _ym_textContainerInset;
  105. self.placeholderTextView.textContainer.lineBreakMode = NSLineBreakByTruncatingTail;
  106. }
  107. - (void)setYm_textContainerInset:(UIEdgeInsets)ym_textContainerInset{
  108. _ym_textContainerInset = ym_textContainerInset;
  109. [self setTextContainerInset:ym_textContainerInset];
  110. [self.placeholderTextView setTextContainerInset:ym_textContainerInset];
  111. }
  112. - (void)textViewHeightDidChangedCalculate {
  113. if (self.textViewHeightDidChanged && self.bounds.size.width > 0 && (self.maxHeight >= self.bounds.size.height || self.maxHeight == 0)) {
  114. if (self.maxHeight == 0) {
  115. self.maxHeight = MAXFLOAT;
  116. }
  117. // 计算高度
  118. NSInteger currentHeight = ceil([self sizeThatFits:CGSizeMake(self.bounds.size.width, MAXFLOAT)].height);
  119. // 如果高度有变化,调用block
  120. if (currentHeight != self.lastHeight) {
  121. // 是否可以滚动
  122. self.scrollEnabled = currentHeight >= self.maxHeight;
  123. CGFloat currentTextViewHeight = currentHeight >= self.maxHeight ? self.maxHeight : currentHeight;
  124. // 改变textView的高度
  125. if (currentTextViewHeight < self.minHeight) currentTextViewHeight = self.minHeight;
  126. CGRect frame = self.frame;
  127. frame.size.height = currentTextViewHeight;
  128. self.frame = frame;
  129. // 调用block
  130. dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
  131. [UIView performWithoutAnimation:^{
  132. if (self.textViewHeightDidChanged) self.textViewHeightDidChanged(self, currentTextViewHeight);
  133. }];
  134. });
  135. // 记录当前高度
  136. self.lastHeight = currentTextViewHeight;
  137. }
  138. }else {
  139. self.needTextViewHeightChanged = YES;
  140. }
  141. }
  142. #pragma mark - Getter
  143. /// 返回一个经过处理的 `self.text` 的值, 去除了首位的空格和换行.
  144. - (NSString *)formatText
  145. {
  146. return [[super text] stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]]; // 去除首尾的空格和换行.
  147. }
  148. #pragma mark - Setter
  149. - (void)setText:(NSString *)text
  150. {
  151. [super setText:text];
  152. self.placeholderTextView.hidden = [@(text.length) boolValue];
  153. // 手动模拟触发通知
  154. NSNotification *notification = [NSNotification notificationWithName:UITextViewTextDidChangeNotification object:self];
  155. [self textDidChange:notification];
  156. }
  157. - (void)setFont:(UIFont *)font
  158. {
  159. [super setFont:font];
  160. self.placeholderTextView.font = font;
  161. }
  162. - (void)setMaxLength:(NSUInteger)maxLength
  163. {
  164. _maxLength = fmax(0, maxLength);
  165. self.text = self.text;
  166. }
  167. - (void)setCornerRadius:(CGFloat)cornerRadius
  168. {
  169. _cornerRadius = cornerRadius;
  170. self.layer.cornerRadius = _cornerRadius;
  171. }
  172. - (void)setBorderColor:(UIColor *)borderColor
  173. {
  174. if (!borderColor) return;
  175. _borderColor = borderColor;
  176. self.layer.borderColor = _borderColor.CGColor;
  177. }
  178. - (void)setBorderWidth:(CGFloat)borderWidth
  179. {
  180. _borderWidth = borderWidth;
  181. self.layer.borderWidth = _borderWidth;
  182. }
  183. - (void)setPlaceholder:(NSString *)placeholder
  184. {
  185. if (!placeholder) return;
  186. _placeholder = [placeholder copy];
  187. if (_placeholder.length > 0) {
  188. self.placeholderTextView.text = _placeholder;
  189. }
  190. }
  191. - (void)setPlaceholderColor:(UIColor *)placeholderColor
  192. {
  193. if (!placeholderColor) return;
  194. _placeholderColor = placeholderColor;
  195. self.placeholderTextView.textColor = _placeholderColor;
  196. }
  197. - (void)setPlaceholderFont:(UIFont *)placeholderFont
  198. {
  199. if (!placeholderFont) return;
  200. _placeholderFont = placeholderFont;
  201. self.placeholderTextView.font = _placeholderFont;
  202. }
  203. - (void)setPlaceholderMaximumNumberOfLines:(NSInteger)placeholderMaximumNumberOfLines {
  204. _placeholderMaximumNumberOfLines = placeholderMaximumNumberOfLines;
  205. self.placeholderTextView.textContainer.maximumNumberOfLines = placeholderMaximumNumberOfLines;
  206. }
  207. - (void)setPlaceholderLineBreakMode:(NSLineBreakMode)placeholderLineBreakMode {
  208. _placeholderLineBreakMode = placeholderLineBreakMode;
  209. self.placeholderTextView.textContainer.lineBreakMode = placeholderLineBreakMode;
  210. }
  211. #pragma mark - NSNotification
  212. - (void)textDidBeginEditing:(NSNotification *)notification {
  213. if (!CGPointEqualToPoint(self.placeholderTextView.contentOffset, CGPointZero)) {
  214. CGSize contentSize = self.placeholderTextView.contentSize;
  215. self.placeholderTextView.contentSize = CGSizeZero;
  216. self.placeholderTextView.contentOffset = CGPointZero;
  217. dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
  218. self.placeholderTextView.contentSize = contentSize;
  219. });
  220. }
  221. !_beginEditingHandler ?: _beginEditingHandler(self);
  222. }
  223. - (void)textDidChange:(NSNotification *)notification
  224. {
  225. // 通知回调的实例的不是当前实例的话直接返回
  226. if (notification.object != self) return;
  227. // 根据字符数量显示或者隐藏 `placeholderLabel`
  228. self.placeholderTextView.hidden = [@(self.text.length) boolValue];
  229. self.placeholderTextView.contentOffset = CGPointZero;
  230. // 禁止第一个字符输入换行
  231. while ([self.text hasPrefix:@"\n"]) {
  232. self.text = [self.text substringFromIndex:1];
  233. }
  234. // 禁止第一个字符输入空格
  235. while (self.disableFirstWhitespace && [self.text hasPrefix:@" "]) {
  236. self.text = [self.text substringFromIndex:1];
  237. }
  238. if (self.disableWhitespace && [self.text containsString:@" "]) {
  239. self.text = [self.text stringByReplacingOccurrencesOfString:@" " withString:@""];
  240. }
  241. if ([self.text containsString:@"\n"]) {
  242. if (self.disableNewline) {
  243. self.text = [self.text stringByReplacingOccurrencesOfString:@"\n" withString:@""];
  244. }
  245. if (self.isResignFirstResponderAfterReturn) {
  246. [self resignFirstResponder];
  247. }
  248. }
  249. // 只有当maxLength字段的值不为无穷大整型也不为0时才计算限制字符数.
  250. if (_maxLength != NSUIntegerMax && _maxLength != 0 && self.text.length > 0) {
  251. if (!self.markedTextRange && self.text.length > _maxLength) {
  252. !_maxHandler ?: _maxHandler(self); // 回调达到最大限制的Block.
  253. self.text = [self.text substringToIndex:_maxLength]; // 截取最大限制字符数.
  254. [self.undoManager removeAllActions]; // 达到最大字符数后清空所有 undoaction, 以免 undo 操作造成crash.
  255. }
  256. }
  257. // 回调文本改变的Block.
  258. !_changeHandler ?: _changeHandler(self);
  259. [self textViewHeightDidChangedCalculate];
  260. }
  261. - (void)textDidEndEditing:(NSNotification *)notification {
  262. !_endEditingHandler ?: _endEditingHandler(self);
  263. }
  264. #pragma mark - Public
  265. + (instancetype)textView
  266. {
  267. return [[self alloc] init];
  268. }
  269. - (void)addTextDidBeginEditingHandler:(YMTextViewHandler)beginEditingHandler
  270. {
  271. _beginEditingHandler = [beginEditingHandler copy];
  272. }
  273. - (void)addTextDidChangeHandler:(YMTextViewHandler)changeHandler
  274. {
  275. _changeHandler = [changeHandler copy];
  276. }
  277. - (void)addTextDidEndEditingHandler:(YMTextViewHandler)endEditingHandler
  278. {
  279. _endEditingHandler = [endEditingHandler copy];
  280. }
  281. - (void)addTextLengthDidMaxHandler:(YMTextViewHandler)maxHandler
  282. {
  283. _maxHandler = [maxHandler copy];
  284. }
  285. @end