SLKTextInputbar.m 32 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909
  1. //
  2. // SlackTextViewController
  3. // https://github.com/slackhq/SlackTextViewController
  4. //
  5. // Copyright 2014-2016 Slack Technologies, Inc.
  6. // Licence: MIT-Licence
  7. //
  8. #import "SLKTextInputbar.h"
  9. #import "SLKTextView.h"
  10. #import "SLKInputAccessoryView.h"
  11. #import "SLKDefaultTypingIndicatorView.h"
  12. #import "SLKTextView+SLKAdditions.h"
  13. #import "UIView+SLKAdditions.h"
  14. #import "SLKUIConstants.h"
  15. NSString * const SLKTextInputbarDidMoveNotification = @"SLKTextInputbarDidMoveNotification";
  16. NSString * const SLKTextInputbarContentSizeDidChangeNotification = @"SLKTextInputbarContentSizeDidChangeNotification";
  17. CGFloat const SLKTextInputbarMinButtonWidth = 44.0;
  18. CGFloat const SLKTextInputbarMinButtonHeight = 44.0;
  19. CGFloat const SLKTextInputbarTypingIndicatorHeight = 24.0;
  20. @interface SLKTextInputbar ()
  21. @property (nonatomic, strong) NSLayoutConstraint *textViewBottomMarginC;
  22. @property (nonatomic, strong) NSLayoutConstraint *contentViewHC;
  23. @property (nonatomic, strong) NSLayoutConstraint *leftButtonWC;
  24. @property (nonatomic, strong) NSLayoutConstraint *leftButtonHC;
  25. @property (nonatomic, strong) NSLayoutConstraint *leftMarginWC;
  26. @property (nonatomic, strong) NSLayoutConstraint *leftButtonBottomMarginC;
  27. @property (nonatomic, strong) NSLayoutConstraint *rightButtonWC;
  28. @property (nonatomic, strong) NSLayoutConstraint *rightButtonHC;
  29. @property (nonatomic, strong) NSLayoutConstraint *rightMarginWC;
  30. @property (nonatomic, strong) NSLayoutConstraint *rightButtonTopMarginC;
  31. @property (nonatomic, strong) NSLayoutConstraint *rightButtonBottomMarginC;
  32. @property (nonatomic, strong) NSLayoutConstraint *editorContentViewHC;
  33. @property (nonatomic, strong) NSLayoutConstraint *typingIndicatorViewHC;
  34. @property (nonatomic, strong) NSLayoutConstraint *typingIndicatorViewTextViewPaddingConstraint;
  35. @property (nonatomic, strong) NSArray *charCountLabelVCs;
  36. @property (nonatomic, assign) UIEdgeInsets defaultInsets;
  37. @property (nonatomic, strong) UILabel *charCountLabel;
  38. @property (nonatomic) CGPoint previousOrigin;
  39. @property (nonatomic, strong) Class textViewClass;
  40. @property (nonatomic, strong) Class typingIndicatorClass;
  41. @property (nonatomic, getter=isHidden) BOOL hidden; // Required override
  42. @end
  43. @implementation SLKTextInputbar
  44. @synthesize textView = _textView;
  45. @synthesize contentView = _contentView;
  46. @synthesize inputAccessoryView = _inputAccessoryView;
  47. @synthesize hidden = _hidden;
  48. #pragma mark - Initialization
  49. - (instancetype)initWithTextViewClass:(Class)textViewClass
  50. {
  51. if (self = [super init]) {
  52. self.textViewClass = textViewClass;
  53. [self slk_commonInit];
  54. }
  55. return self;
  56. }
  57. - (instancetype)initWithTextViewClass:(Class)textViewClass withTypingIndicatorViewClass:(Class)typingIndicatorClass
  58. {
  59. if (self = [super init]) {
  60. self.textViewClass = textViewClass;
  61. self.typingIndicatorClass = typingIndicatorClass;
  62. [self slk_commonInit];
  63. }
  64. return self;
  65. }
  66. - (id)init
  67. {
  68. if (self = [super init]) {
  69. [self slk_commonInit];
  70. }
  71. return self;
  72. }
  73. - (instancetype)initWithCoder:(NSCoder *)coder
  74. {
  75. if (self = [super initWithCoder:coder]) {
  76. [self slk_commonInit];
  77. }
  78. return self;
  79. }
  80. - (void)slk_commonInit
  81. {
  82. self.charCountLabelNormalColor = [UIColor lightGrayColor];
  83. self.charCountLabelWarningColor = [UIColor redColor];
  84. self.autoHideRightButton = YES;
  85. self.editorContentViewHeight = 38.0;
  86. self.defaultInsets = UIEdgeInsetsMake(5.0, 8.0, 5.0, 8.0);
  87. self.contentInset = _defaultInsets;
  88. // Since iOS 11, it is required to call -layoutSubviews before adding custom subviews
  89. // so private UIToolbar subviews don't interfere on the touch hierarchy
  90. [self layoutSubviews];
  91. [self addSubview:self.typingView];
  92. [self addSubview:self.editorContentView];
  93. [self addSubview:self.leftButton];
  94. [self addSubview:self.rightButton];
  95. [self addSubview:self.textView];
  96. [self addSubview:self.charCountLabel];
  97. [self addSubview:self.contentView];
  98. [self slk_setupViewConstraints];
  99. [self slk_updateConstraintConstants];
  100. self.counterStyle = SLKCounterStyleNone;
  101. self.counterPosition = SLKCounterPositionTop;
  102. [self slk_registerNotifications];
  103. [self slk_registerTo:self.layer forSelector:@selector(position)];
  104. [self slk_registerTo:self.leftButton.imageView forSelector:@selector(image)];
  105. [self slk_registerTo:self.rightButton.titleLabel forSelector:@selector(font)];
  106. }
  107. #pragma mark - UIView Overrides
  108. - (void)layoutIfNeeded
  109. {
  110. if (self.constraints.count == 0 || !self.window) {
  111. return;
  112. }
  113. [self slk_updateConstraintConstants];
  114. [super layoutIfNeeded];
  115. }
  116. - (CGSize)intrinsicContentSize
  117. {
  118. return CGSizeMake(UIViewNoIntrinsicMetric, [self minimumInputbarHeight]);
  119. }
  120. + (BOOL)requiresConstraintBasedLayout
  121. {
  122. return YES;
  123. }
  124. - (void)safeAreaInsetsDidChange
  125. {
  126. UIEdgeInsets safeAreaInsets = self.safeAreaInsets;
  127. self.contentInset = UIEdgeInsetsMake(_defaultInsets.top + safeAreaInsets.top,
  128. _defaultInsets.left + safeAreaInsets.left,
  129. _defaultInsets.bottom + safeAreaInsets.bottom,
  130. _defaultInsets.right + safeAreaInsets.right);
  131. }
  132. #pragma mark - Getters
  133. - (SLKTextView *)textView
  134. {
  135. if (!_textView) {
  136. Class class = self.textViewClass ? : [SLKTextView class];
  137. _textView = [[class alloc] init];
  138. _textView.translatesAutoresizingMaskIntoConstraints = NO;
  139. _textView.font = [UIFont systemFontOfSize:15.0];
  140. _textView.maxNumberOfLines = [self slk_defaultNumberOfLines];
  141. _textView.keyboardType = UIKeyboardTypeTwitter;
  142. _textView.returnKeyType = UIReturnKeyDefault;
  143. _textView.enablesReturnKeyAutomatically = YES;
  144. _textView.scrollIndicatorInsets = UIEdgeInsetsMake(0.0, -1.0, 0.0, 1.0);
  145. _textView.textContainerInset = UIEdgeInsetsMake(8.0, 4.0, 8.0, 0.0);
  146. _textView.layer.cornerRadius = 5.0;
  147. _textView.layer.borderWidth = 0.5;
  148. _textView.layer.borderColor = [UIColor colorWithRed:200.0/255.0 green:200.0/255.0 blue:205.0/255.0 alpha:1.0].CGColor;
  149. }
  150. return _textView;
  151. }
  152. - (UIView *)contentView
  153. {
  154. if (!_contentView) {
  155. _contentView = [UIView new];
  156. _contentView.translatesAutoresizingMaskIntoConstraints = NO;
  157. _contentView.backgroundColor = [UIColor clearColor];
  158. _contentView.clipsToBounds = YES;
  159. }
  160. return _contentView;
  161. }
  162. - (SLKInputAccessoryView *)inputAccessoryView
  163. {
  164. if (!_inputAccessoryView) {
  165. _inputAccessoryView = [[SLKInputAccessoryView alloc] initWithFrame:CGRectZero];
  166. _inputAccessoryView.backgroundColor = [UIColor clearColor];
  167. _inputAccessoryView.userInteractionEnabled = NO;
  168. }
  169. return _inputAccessoryView;
  170. }
  171. - (UIButton *)leftButton
  172. {
  173. if (!_leftButton) {
  174. _leftButton = [UIButton buttonWithType:UIButtonTypeSystem];
  175. _leftButton.translatesAutoresizingMaskIntoConstraints = NO;
  176. _leftButton.titleLabel.font = [UIFont systemFontOfSize:15.0];
  177. }
  178. return _leftButton;
  179. }
  180. - (UIButton *)rightButton
  181. {
  182. if (!_rightButton) {
  183. _rightButton = [UIButton buttonWithType:UIButtonTypeSystem];
  184. _rightButton.translatesAutoresizingMaskIntoConstraints = NO;
  185. _rightButton.titleLabel.font = [UIFont boldSystemFontOfSize:15.0];
  186. _rightButton.enabled = NO;
  187. NSString *title = NSLocalizedString(@"Send", nil);
  188. [_rightButton setTitle:title forState:UIControlStateNormal];
  189. }
  190. return _rightButton;
  191. }
  192. - (UIView *)editorContentView
  193. {
  194. if (!_editorContentView) {
  195. _editorContentView = [UIView new];
  196. _editorContentView.translatesAutoresizingMaskIntoConstraints = NO;
  197. _editorContentView.backgroundColor = self.backgroundColor;
  198. _editorContentView.clipsToBounds = YES;
  199. _editorContentView.hidden = YES;
  200. [_editorContentView addSubview:self.editorTitle];
  201. [_editorContentView addSubview:self.editorLeftButton];
  202. [_editorContentView addSubview:self.editorRightButton];
  203. NSDictionary *views = @{@"label": self.editorTitle,
  204. @"leftButton": self.editorLeftButton,
  205. @"rightButton": self.editorRightButton,
  206. };
  207. NSDictionary *metrics = @{@"left" : @(self.contentInset.left),
  208. @"right" : @(self.contentInset.right)
  209. };
  210. [_editorContentView addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"H:|-(left)-[leftButton(60)]-(left)-[label(>=0)]-(right)-[rightButton(60)]-(<=right)-|" options:0 metrics:metrics views:views]];
  211. [_editorContentView addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"V:|[leftButton]|" options:0 metrics:metrics views:views]];
  212. [_editorContentView addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"V:|[rightButton]|" options:0 metrics:metrics views:views]];
  213. [_editorContentView addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"V:|[label]|" options:0 metrics:metrics views:views]];
  214. }
  215. return _editorContentView;
  216. }
  217. - (UILabel *)editorTitle
  218. {
  219. if (!_editorTitle) {
  220. _editorTitle = [UILabel new];
  221. _editorTitle.translatesAutoresizingMaskIntoConstraints = NO;
  222. _editorTitle.textAlignment = NSTextAlignmentCenter;
  223. _editorTitle.backgroundColor = [UIColor clearColor];
  224. _editorTitle.font = [UIFont boldSystemFontOfSize:15.0];
  225. NSString *title = NSLocalizedString(@"Editing Message", nil);
  226. _editorTitle.text = title;
  227. }
  228. return _editorTitle;
  229. }
  230. - (UIButton *)editorLeftButton
  231. {
  232. if (!_editorLeftButton) {
  233. _editorLeftButton = [UIButton buttonWithType:UIButtonTypeSystem];
  234. _editorLeftButton.translatesAutoresizingMaskIntoConstraints = NO;
  235. _editorLeftButton.contentHorizontalAlignment = UIControlContentHorizontalAlignmentLeft;
  236. _editorLeftButton.titleLabel.font = [UIFont systemFontOfSize:15.0];
  237. NSString *title = NSLocalizedString(@"Cancel", nil);
  238. [_editorLeftButton setTitle:title forState:UIControlStateNormal];
  239. }
  240. return _editorLeftButton;
  241. }
  242. - (UIButton *)editorRightButton
  243. {
  244. if (!_editorRightButton) {
  245. _editorRightButton = [UIButton buttonWithType:UIButtonTypeSystem];
  246. _editorRightButton.translatesAutoresizingMaskIntoConstraints = NO;
  247. _editorRightButton.contentHorizontalAlignment = UIControlContentHorizontalAlignmentRight;
  248. _editorRightButton.titleLabel.font = [UIFont boldSystemFontOfSize:15.0];
  249. _editorRightButton.enabled = NO;
  250. NSString *title = NSLocalizedString(@"Save", nil);
  251. [_editorRightButton setTitle:title forState:UIControlStateNormal];
  252. }
  253. return _editorRightButton;
  254. }
  255. - (UILabel *)charCountLabel
  256. {
  257. if (!_charCountLabel) {
  258. _charCountLabel = [UILabel new];
  259. _charCountLabel.translatesAutoresizingMaskIntoConstraints = NO;
  260. _charCountLabel.backgroundColor = [UIColor clearColor];
  261. _charCountLabel.textAlignment = NSTextAlignmentRight;
  262. _charCountLabel.font = [UIFont systemFontOfSize:11.0];
  263. _charCountLabel.hidden = NO;
  264. }
  265. return _charCountLabel;
  266. }
  267. - (UIView *)typingView
  268. {
  269. if (!_typingView) {
  270. if (self.typingIndicatorClass == nil) {
  271. _typingView = [[SLKDefaultTypingIndicatorView alloc] init];
  272. } else {
  273. Class class = self.typingIndicatorClass;
  274. _typingView = [[class alloc] init];
  275. _typingView.translatesAutoresizingMaskIntoConstraints = NO;
  276. _typingView.clipsToBounds = YES;
  277. [_typingView addObserver:self forKeyPath:@"visible" options:NSKeyValueObservingOptionNew context:nil];
  278. }
  279. }
  280. return _typingView;
  281. }
  282. - (BOOL)isHidden
  283. {
  284. return _hidden;
  285. }
  286. - (CGFloat)minimumInputbarHeight
  287. {
  288. CGFloat minimumHeight = self.textView.intrinsicContentSize.height;
  289. minimumHeight += self.contentInset.top;
  290. minimumHeight += self.slk_bottomMargin;
  291. minimumHeight += [self slk_typingIndicatorHeight];
  292. return minimumHeight;
  293. }
  294. - (CGFloat)appropriateHeight
  295. {
  296. CGFloat height = 0.0;
  297. CGFloat minimumHeight = [self minimumInputbarHeight];
  298. if (self.textView.numberOfLines == 1) {
  299. height = minimumHeight;
  300. }
  301. else if (self.textView.numberOfLines < self.textView.maxNumberOfLines) {
  302. height = [self slk_inputBarHeightForLines:self.textView.numberOfLines];
  303. }
  304. else {
  305. height = [self slk_inputBarHeightForLines:self.textView.maxNumberOfLines];
  306. }
  307. if (height < minimumHeight) {
  308. height = minimumHeight;
  309. }
  310. if (self.isEditing) {
  311. height += self.editorContentViewHeight;
  312. }
  313. return roundf(height);
  314. }
  315. - (BOOL)limitExceeded
  316. {
  317. NSString *text = [self.textView.text stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]];
  318. if (self.maxCharCount > 0 && text.length > self.maxCharCount) {
  319. return YES;
  320. }
  321. return NO;
  322. }
  323. - (CGFloat)slk_inputBarHeightForLines:(NSUInteger)numberOfLines
  324. {
  325. CGFloat height = self.textView.intrinsicContentSize.height;
  326. // Since the intrinsicContentSize (see SLKTextView) contains the lineHeight, we remove it here again
  327. height -= self.textView.font.lineHeight;
  328. height += roundf(self.textView.font.lineHeight * numberOfLines);
  329. height += roundf(self.textView.font.leading * numberOfLines);
  330. height += self.contentInset.top;
  331. height += self.slk_bottomMargin;
  332. height += [self slk_typingIndicatorHeight];
  333. return height;
  334. }
  335. - (CGFloat)slk_bottomMargin
  336. {
  337. CGFloat margin = self.contentInset.bottom;
  338. margin += self.slk_contentViewHeight;
  339. return margin;
  340. }
  341. - (CGFloat)slk_contentViewHeight
  342. {
  343. if (!self.editing) {
  344. return CGRectGetHeight(self.contentView.frame);
  345. }
  346. return 0.0;
  347. }
  348. - (CGFloat)slk_textViewHeight
  349. {
  350. return [self slk_contentViewHeight] - [self slk_typingIndicatorHeight];
  351. }
  352. - (CGFloat)slk_typingIndicatorHeight
  353. {
  354. return self.typingIndicatorViewHC.constant + self.typingIndicatorViewTextViewPaddingConstraint.constant;
  355. }
  356. - (CGFloat)slk_appropriateRightButtonWidth
  357. {
  358. if (self.autoHideRightButton) {
  359. if (self.textView.text.length == 0) {
  360. return 0.0;
  361. }
  362. }
  363. CGFloat width = [self.rightButton intrinsicContentSize].width;
  364. width = (width >= SLKTextInputbarMinButtonWidth) ? width : SLKTextInputbarMinButtonWidth;
  365. return width;
  366. }
  367. - (CGFloat)slk_appropriateRightButtonMargin
  368. {
  369. if (self.autoHideRightButton) {
  370. if (self.textView.text.length == 0) {
  371. return 0.0;
  372. }
  373. }
  374. return self.contentInset.right;
  375. }
  376. - (NSUInteger)slk_defaultNumberOfLines
  377. {
  378. if (SLK_IS_IPAD) {
  379. return 8;
  380. }
  381. else {
  382. return 6;
  383. }
  384. }
  385. #pragma mark - Setters
  386. - (void)setBackgroundColor:(UIColor *)color
  387. {
  388. self.barTintColor = color;
  389. self.editorContentView.backgroundColor = color;
  390. }
  391. - (void)setAutoHideRightButton:(BOOL)hide
  392. {
  393. if (self.autoHideRightButton == hide) {
  394. return;
  395. }
  396. _autoHideRightButton = hide;
  397. self.rightButtonWC.constant = [self slk_appropriateRightButtonWidth];
  398. self.rightMarginWC.constant = [self slk_appropriateRightButtonMargin];
  399. [self layoutIfNeeded];
  400. }
  401. - (void)setContentInset:(UIEdgeInsets)insets
  402. {
  403. if (UIEdgeInsetsEqualToEdgeInsets(self.contentInset, insets)) {
  404. return;
  405. }
  406. if (UIEdgeInsetsEqualToEdgeInsets(self.contentInset, UIEdgeInsetsZero)) {
  407. _contentInset = insets;
  408. return;
  409. }
  410. _contentInset = insets;
  411. // Add new constraints
  412. [self removeConstraints:self.constraints];
  413. [self slk_setupViewConstraints];
  414. [self setCounterPosition:_counterPosition];
  415. // Add constant values and refresh layout
  416. [self slk_updateConstraintConstants];
  417. [super layoutIfNeeded];
  418. }
  419. - (void)setEditing:(BOOL)editing
  420. {
  421. if (self.isEditing == editing) {
  422. return;
  423. }
  424. _editing = editing;
  425. _editorContentView.hidden = !editing;
  426. self.contentViewHC.active = editing;
  427. [super setNeedsLayout];
  428. [super layoutIfNeeded];
  429. }
  430. - (void)setHidden:(BOOL)hidden
  431. {
  432. // We don't call super here, since we want to avoid to visually hide the view.
  433. // The hidden render state is handled by the view controller.
  434. _hidden = hidden;
  435. if (!self.isEditing) {
  436. self.contentViewHC.active = hidden;
  437. [self slk_updateConstraintConstants];
  438. [super setNeedsLayout];
  439. [super layoutIfNeeded];
  440. }
  441. }
  442. - (void)setCounterPosition:(SLKCounterPosition)counterPosition
  443. {
  444. // Clears the previous constraints
  445. if (_charCountLabelVCs.count > 0) {
  446. [self removeConstraints:_charCountLabelVCs];
  447. _charCountLabelVCs = nil;
  448. }
  449. _counterPosition = counterPosition;
  450. NSDictionary *views = @{@"rightButton": self.rightButton,
  451. @"charCountLabel": self.charCountLabel
  452. };
  453. NSDictionary *metrics = @{@"top" : @(self.contentInset.top),
  454. @"bottom" : @(-self.slk_bottomMargin/2.0)
  455. };
  456. // Constraints are different depending of the counter's position type
  457. if (counterPosition == SLKCounterPositionBottom) {
  458. _charCountLabelVCs = [NSLayoutConstraint constraintsWithVisualFormat:@"V:[charCountLabel]-(bottom)-[rightButton]" options:0 metrics:metrics views:views];
  459. }
  460. else {
  461. _charCountLabelVCs = [NSLayoutConstraint constraintsWithVisualFormat:@"V:|-(top@750)-[charCountLabel]-(>=0)-|" options:0 metrics:metrics views:views];
  462. }
  463. [self addConstraints:self.charCountLabelVCs];
  464. }
  465. #pragma mark - Text Editing
  466. - (BOOL)canEditText:(NSString *)text
  467. {
  468. if ((self.isEditing && [self.textView.text isEqualToString:text]) || self.isHidden) {
  469. return NO;
  470. }
  471. return YES;
  472. }
  473. - (void)beginTextEditing
  474. {
  475. if (self.isEditing || self.isHidden) {
  476. return;
  477. }
  478. self.editing = YES;
  479. [self slk_updateConstraintConstants];
  480. if (!self.isFirstResponder) {
  481. [self layoutIfNeeded];
  482. }
  483. }
  484. - (void)endTextEdition
  485. {
  486. if (!self.isEditing || self.isHidden) {
  487. return;
  488. }
  489. self.editing = NO;
  490. [self slk_updateConstraintConstants];
  491. }
  492. #pragma mark - Character Counter
  493. - (void)slk_updateCounter
  494. {
  495. NSString *text = [self.textView.text stringByTrimmingCharactersInSet:[NSCharacterSet newlineCharacterSet]];
  496. NSString *counter = nil;
  497. if (self.counterStyle == SLKCounterStyleNone) {
  498. counter = [NSString stringWithFormat:@"%lu", (unsigned long)text.length];
  499. }
  500. if (self.counterStyle == SLKCounterStyleSplit) {
  501. counter = [NSString stringWithFormat:@"%lu/%lu", (unsigned long)text.length, (unsigned long)self.maxCharCount];
  502. }
  503. if (self.counterStyle == SLKCounterStyleCountdown) {
  504. counter = [NSString stringWithFormat:@"%ld", (long)(text.length - self.maxCharCount)];
  505. }
  506. if (self.counterStyle == SLKCounterStyleCountdownReversed)
  507. {
  508. counter = [NSString stringWithFormat:@"%ld", (long)(self.maxCharCount - text.length)];
  509. }
  510. if (self.counterStyle == SLKCounterStyleLimitExceeded)
  511. {
  512. counter = [self limitExceeded] ? [NSString stringWithFormat:@"%ld", (long)(self.maxCharCount - text.length)] : @"";
  513. }
  514. self.charCountLabel.text = counter;
  515. self.charCountLabel.textColor = [self limitExceeded] ? self.charCountLabelWarningColor : self.charCountLabelNormalColor;
  516. }
  517. #pragma mark - Notification Events
  518. - (void)slk_didChangeTextViewText:(NSNotification *)notification
  519. {
  520. SLKTextView *textView = (SLKTextView *)notification.object;
  521. // Skips this it's not the expected textView.
  522. if (![textView isEqual:self.textView]) {
  523. return;
  524. }
  525. // Updates the char counter label
  526. if (self.maxCharCount > 0) {
  527. [self slk_updateCounter];
  528. }
  529. if (self.autoHideRightButton && !self.isEditing)
  530. {
  531. CGFloat rightButtonNewWidth = [self slk_appropriateRightButtonWidth];
  532. // Only updates if the width did change
  533. if (self.rightButtonWC.constant == rightButtonNewWidth) {
  534. return;
  535. }
  536. self.rightButtonWC.constant = rightButtonNewWidth;
  537. self.rightMarginWC.constant = [self slk_appropriateRightButtonMargin];
  538. [self.rightButton layoutIfNeeded]; // Avoids the right button to stretch when animating the constraint changes
  539. BOOL bounces = self.bounces && [self.textView isFirstResponder];
  540. if (self.window) {
  541. [self slk_animateLayoutIfNeededWithBounce:bounces
  542. options:UIViewAnimationOptionCurveEaseInOut|UIViewAnimationOptionBeginFromCurrentState|UIViewAnimationOptionAllowUserInteraction
  543. animations:NULL];
  544. }
  545. else {
  546. [self layoutIfNeeded];
  547. }
  548. }
  549. }
  550. - (void)slk_didChangeTextViewContentSize:(NSNotification *)notification
  551. {
  552. if (self.maxCharCount > 0) {
  553. BOOL shouldHide = (self.textView.numberOfLines == 1) || self.editing;
  554. self.charCountLabel.hidden = shouldHide;
  555. }
  556. }
  557. - (void)slk_didChangeContentSizeCategory:(NSNotification *)notification
  558. {
  559. if (!self.textView.isDynamicTypeEnabled) {
  560. return;
  561. }
  562. [self layoutIfNeeded];
  563. }
  564. #pragma mark - View Auto-Layout
  565. - (void)slk_setupViewConstraints
  566. {
  567. NSDictionary *views = @{@"textView": self.textView,
  568. @"leftButton": self.leftButton,
  569. @"rightButton": self.rightButton,
  570. @"editorContentView": self.editorContentView,
  571. @"charCountLabel": self.charCountLabel,
  572. @"contentView": self.contentView,
  573. @"typingView": self.typingView
  574. };
  575. NSDictionary *metrics = @{@"top" : @(self.contentInset.top),
  576. @"left" : @(self.contentInset.left),
  577. @"right" : @(self.contentInset.right),
  578. @"buttonMargin" : @(MIN(self.contentInset.left, self.contentInset.right)),
  579. };
  580. [self addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"H:|-(left)-[leftButton(0)]-(<=buttonMargin)-[textView]-(buttonMargin)-[rightButton(0)]-(right)-|" options:0 metrics:metrics views:views]];
  581. [self addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"V:|-(>=0)-[leftButton(0)]-(0@750)-|" options:0 metrics:metrics views:views]];
  582. [self addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"V:|-(>=0)-[rightButton(0)]-(<=0)-|" options:0 metrics:metrics views:views]];
  583. [self addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"H:|-(left@250)-[charCountLabel(<=50@1000)]-(right@750)-|" options:0 metrics:metrics views:views]];
  584. [self addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"H:|-(50)-[typingView]|" options:0 metrics:metrics views:views]];
  585. [self addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"H:|[editorContentView]|" options:0 metrics:metrics views:views]];
  586. [self addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"H:|[contentView]|" options:0 metrics:metrics views:views]];
  587. [self addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"V:[contentView(0)]|" options:0 metrics:metrics views:views]];
  588. NSArray<NSLayoutConstraint *> *verticalTypingIndicatorTextViewContraints = [NSLayoutConstraint constraintsWithVisualFormat:@"V:|-0-[editorContentView(0)]-(<=top)-[typingView(0)]-(<=0)-[textView(0@999)]-(0)-|" options:0 metrics:metrics views:views];
  589. self.typingIndicatorViewTextViewPaddingConstraint = verticalTypingIndicatorTextViewContraints[4];
  590. [self addConstraints:verticalTypingIndicatorTextViewContraints];
  591. self.textViewBottomMarginC = [self slk_constraintForAttribute:NSLayoutAttributeBottom firstItem:self secondItem:self.textView];
  592. self.editorContentViewHC = [self slk_constraintForAttribute:NSLayoutAttributeHeight firstItem:self.editorContentView secondItem:nil];
  593. self.typingIndicatorViewHC = [self slk_constraintForAttribute:NSLayoutAttributeHeight firstItem:self.typingView secondItem:nil];
  594. self.contentViewHC = [self slk_constraintForAttribute:NSLayoutAttributeHeight firstItem:self.contentView secondItem:nil];;
  595. self.contentViewHC.active = NO; // Disabled by default, so the height is calculated with the height of its subviews
  596. self.leftButtonWC = [self slk_constraintForAttribute:NSLayoutAttributeWidth firstItem:self.leftButton secondItem:nil];
  597. self.leftButtonHC = [self slk_constraintForAttribute:NSLayoutAttributeHeight firstItem:self.leftButton secondItem:nil];
  598. self.leftButtonBottomMarginC = [self slk_constraintForAttribute:NSLayoutAttributeBottom firstItem:self secondItem:self.leftButton];
  599. self.leftMarginWC = [[self slk_constraintsForAttribute:NSLayoutAttributeLeading] firstObject];
  600. self.rightButtonWC = [self slk_constraintForAttribute:NSLayoutAttributeWidth firstItem:self.rightButton secondItem:nil];
  601. self.rightButtonHC = [self slk_constraintForAttribute:NSLayoutAttributeHeight firstItem:self.rightButton secondItem:nil];
  602. self.rightMarginWC = [[self slk_constraintsForAttribute:NSLayoutAttributeTrailing] firstObject];
  603. self.rightButtonTopMarginC = [self slk_constraintForAttribute:NSLayoutAttributeTop firstItem:self.rightButton secondItem:self];
  604. self.rightButtonBottomMarginC = [self slk_constraintForAttribute:NSLayoutAttributeBottom firstItem:self secondItem:self.rightButton];
  605. }
  606. - (void)slk_updateConstraintConstants
  607. {
  608. CGFloat zero = 0.0;
  609. self.textViewBottomMarginC.constant = self.slk_bottomMargin;
  610. if (self.isEditing)
  611. {
  612. self.editorContentViewHC.constant = self.editorContentViewHeight;
  613. self.leftButtonWC.constant = zero;
  614. self.leftButtonHC.constant = zero;
  615. self.leftMarginWC.constant = zero;
  616. self.leftButtonBottomMarginC.constant = zero;
  617. self.rightButtonWC.constant = zero;
  618. self.rightButtonHC.constant = zero;
  619. self.rightMarginWC.constant = zero;
  620. }
  621. else {
  622. self.editorContentViewHC.constant = zero;
  623. // When the inputbar is hidden, we need to hide the buttons as well
  624. if (self->_hidden) {
  625. self.leftButtonHC.constant = zero;
  626. self.rightButtonHC.constant = zero;
  627. return;
  628. }
  629. CGSize leftButtonSize = [self.leftButton imageForState:self.leftButton.state].size;
  630. CGSize rightButtonSize = [self.rightButton imageForState:self.rightButton.state].size;
  631. if (leftButtonSize.width > 0) {
  632. leftButtonSize.width = (leftButtonSize.width >= SLKTextInputbarMinButtonWidth) ? leftButtonSize.width : SLKTextInputbarMinButtonWidth;
  633. float leftButtonHeight = (leftButtonSize.height >= SLKTextInputbarMinButtonHeight) ? leftButtonSize.height : SLKTextInputbarMinButtonHeight;
  634. self.leftButtonHC.constant = roundf(leftButtonHeight);
  635. self.leftButtonBottomMarginC.constant = roundf((self.intrinsicContentSize.height - leftButtonHeight) / 2.0) + self.slk_textViewHeight / 2.0;
  636. }
  637. self.leftButtonWC.constant = roundf(leftButtonSize.width);
  638. self.leftMarginWC.constant = (leftButtonSize.width > 0) ? self.contentInset.left : 16;
  639. self.rightButtonWC.constant = [self slk_appropriateRightButtonWidth];
  640. self.rightMarginWC.constant = [self slk_appropriateRightButtonMargin];
  641. float rightButtonHeight = (rightButtonSize.height >= SLKTextInputbarMinButtonHeight) ? rightButtonSize.height : SLKTextInputbarMinButtonHeight;
  642. self.rightButtonHC.constant = roundf(rightButtonHeight);
  643. self.rightButtonBottomMarginC.constant = roundf((self.intrinsicContentSize.height - rightButtonHeight) / 2.0) + self.slk_textViewHeight / 2.0;
  644. }
  645. }
  646. #pragma mark - Observers
  647. - (void)slk_registerTo:(id)object forSelector:(SEL)selector
  648. {
  649. if (object) {
  650. [object addObserver:self forKeyPath:NSStringFromSelector(selector) options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld context:NULL];
  651. }
  652. }
  653. - (void)slk_unregisterFrom:(id)object forSelector:(SEL)selector
  654. {
  655. if (object) {
  656. [object removeObserver:self forKeyPath:NSStringFromSelector(selector)];
  657. }
  658. }
  659. - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
  660. {
  661. if ([object isEqual:self.layer] && [keyPath isEqualToString:NSStringFromSelector(@selector(position))]) {
  662. if (!CGPointEqualToPoint(self.previousOrigin, self.frame.origin)) {
  663. self.previousOrigin = self.frame.origin;
  664. [[NSNotificationCenter defaultCenter] postNotificationName:SLKTextInputbarDidMoveNotification object:self userInfo:@{@"origin": [NSValue valueWithCGPoint:self.previousOrigin]}];
  665. }
  666. }
  667. else if ([object isEqual:self.leftButton.imageView] && [keyPath isEqualToString:NSStringFromSelector(@selector(image))]) {
  668. UIImage *newImage = change[NSKeyValueChangeNewKey];
  669. UIImage *oldImage = change[NSKeyValueChangeOldKey];
  670. if (![newImage isEqual:oldImage]) {
  671. [self slk_updateConstraintConstants];
  672. }
  673. }
  674. else if ([object isEqual:self.rightButton.titleLabel] && [keyPath isEqualToString:NSStringFromSelector(@selector(font))]) {
  675. [self slk_updateConstraintConstants];
  676. }
  677. else if ([object conformsToProtocol:@protocol(SLKVisibleViewProtocol)] && [keyPath isEqualToString:@"visible"]) {
  678. [self slk_animateLayoutIfNeededWithBounce:NO options:UIViewAnimationOptionCurveEaseInOut|UIViewAnimationOptionLayoutSubviews|UIViewAnimationOptionBeginFromCurrentState animations:^{
  679. if (self.typingView.isVisible) {
  680. self.typingIndicatorViewHC.constant = SLKTextInputbarTypingIndicatorHeight;
  681. self.typingIndicatorViewTextViewPaddingConstraint.constant = self.contentInset.top;
  682. } else {
  683. self.typingIndicatorViewHC.constant = 0;
  684. self.typingIndicatorViewTextViewPaddingConstraint.constant = 0;
  685. }
  686. // Make sure we update the scrollView position in the viewController as well
  687. [[NSNotificationCenter defaultCenter] postNotificationName:SLKTextInputbarContentSizeDidChangeNotification object:self];
  688. }];
  689. }
  690. else {
  691. [super observeValueForKeyPath:keyPath ofObject:object change:change context:context];
  692. }
  693. }
  694. #pragma mark - NSNotificationCenter registration
  695. - (void)slk_registerNotifications
  696. {
  697. [self slk_unregisterNotifications];
  698. [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(slk_didChangeTextViewText:) name:UITextViewTextDidChangeNotification object:nil];
  699. [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(slk_didChangeTextViewContentSize:) name:SLKTextViewContentSizeDidChangeNotification object:nil];
  700. [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(slk_didChangeContentSizeCategory:) name:UIContentSizeCategoryDidChangeNotification object:nil];
  701. }
  702. - (void)slk_unregisterNotifications
  703. {
  704. [[NSNotificationCenter defaultCenter] removeObserver:self name:UITextViewTextDidChangeNotification object:nil];
  705. [[NSNotificationCenter defaultCenter] removeObserver:self name:SLKTextViewContentSizeDidChangeNotification object:nil];
  706. [[NSNotificationCenter defaultCenter] removeObserver:self name:UIContentSizeCategoryDidChangeNotification object:nil];
  707. }
  708. #pragma mark - Lifeterm
  709. - (void)dealloc
  710. {
  711. [self slk_unregisterNotifications];
  712. [self slk_unregisterFrom:self.layer forSelector:@selector(position)];
  713. [self slk_unregisterFrom:self.leftButton.imageView forSelector:@selector(image)];
  714. [self slk_unregisterFrom:self.rightButton.titleLabel forSelector:@selector(font)];
  715. [self.typingView removeObserver:self forKeyPath:@"visible"];
  716. }
  717. @end