SLKTextViewController.m 79 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712171317141715171617171718171917201721172217231724172517261727172817291730173117321733173417351736173717381739174017411742174317441745174617471748174917501751175217531754175517561757175817591760176117621763176417651766176717681769177017711772177317741775177617771778177917801781178217831784178517861787178817891790179117921793179417951796179717981799180018011802180318041805180618071808180918101811181218131814181518161817181818191820182118221823182418251826182718281829183018311832183318341835183618371838183918401841184218431844184518461847184818491850185118521853185418551856185718581859186018611862186318641865186618671868186918701871187218731874187518761877187818791880188118821883188418851886188718881889189018911892189318941895189618971898189919001901190219031904190519061907190819091910191119121913191419151916191719181919192019211922192319241925192619271928192919301931193219331934193519361937193819391940194119421943194419451946194719481949195019511952195319541955195619571958195919601961196219631964196519661967196819691970197119721973197419751976197719781979198019811982198319841985198619871988198919901991199219931994199519961997199819992000200120022003200420052006200720082009201020112012201320142015201620172018201920202021202220232024202520262027202820292030203120322033203420352036203720382039204020412042204320442045204620472048204920502051205220532054205520562057205820592060206120622063206420652066206720682069207020712072207320742075207620772078207920802081208220832084208520862087208820892090209120922093209420952096209720982099210021012102210321042105210621072108210921102111211221132114211521162117211821192120212121222123212421252126212721282129213021312132213321342135213621372138213921402141214221432144214521462147214821492150215121522153215421552156215721582159216021612162216321642165216621672168216921702171217221732174217521762177217821792180218121822183218421852186218721882189219021912192219321942195219621972198219922002201220222032204220522062207220822092210221122122213221422152216221722182219222022212222222322242225222622272228222922302231223222332234223522362237223822392240224122422243224422452246224722482249225022512252225322542255225622572258225922602261226222632264226522662267226822692270227122722273227422752276227722782279228022812282228322842285228622872288228922902291229222932294229522962297229822992300230123022303230423052306230723082309231023112312231323142315231623172318231923202321232223232324232523262327232823292330233123322333233423352336233723382339234023412342234323442345234623472348234923502351235223532354
  1. //
  2. // SlackTextViewController
  3. // https://github.com/slackhq/SlackTextViewController
  4. //
  5. // Copyright 2014-2016 Slack Technologies, Inc.
  6. // Licence: MIT-Licence
  7. //
  8. #import "SLKTextViewController.h"
  9. #import "SLKInputAccessoryView.h"
  10. #import "SLKDefaultReplyView.h"
  11. #import "UIResponder+SLKAdditions.h"
  12. #import "SLKUIConstants.h"
  13. #define kSLKAlertViewClearTextTag [NSStringFromClass([SLKTextViewController class]) hash]
  14. NSString * const SLKKeyboardWillShowNotification = @"SLKKeyboardWillShowNotification";
  15. NSString * const SLKKeyboardDidShowNotification = @"SLKKeyboardDidShowNotification";
  16. NSString * const SLKKeyboardWillHideNotification = @"SLKKeyboardWillHideNotification";
  17. NSString * const SLKKeyboardDidHideNotification = @"SLKKeyboardDidHideNotification";
  18. CGFloat const SLKAutoCompletionViewDefaultHeight = 140.0;
  19. @interface SLKTextViewController ()
  20. {
  21. CGPoint _scrollViewOffsetBeforeDragging;
  22. CGFloat _keyboardHeightBeforeDragging;
  23. }
  24. // The shared scrollView pointer, either a tableView or collectionView
  25. @property (nonatomic, weak) UIScrollView *scrollViewProxy;
  26. // A hairline displayed on top of the auto-completion view, to better separate the content from the control.
  27. @property (nonatomic, strong) UIView *autoCompletionHairline;
  28. // Auto-Layout height constraints used for updating their constants
  29. @property (nonatomic, strong) NSLayoutConstraint *scrollViewHC;
  30. @property (nonatomic, strong) NSLayoutConstraint *textInputbarHC;
  31. @property (nonatomic, strong) NSLayoutConstraint *replyViewHC;
  32. @property (nonatomic, strong) NSLayoutConstraint *autoCompletionViewHC;
  33. @property (nonatomic, strong) NSLayoutConstraint *keyboardHC;
  34. // YES if the user is moving the keyboard with a gesture
  35. @property (nonatomic, assign, getter = isMovingKeyboard) BOOL movingKeyboard;
  36. // YES if the view controller did appear and everything is finished configurating. This allows blocking some layout animations among other things.
  37. @property (nonatomic, getter=isViewVisible) BOOL viewVisible;
  38. // YES if the view controller's view's size is changing by its parent (i.e. when its window rotates or is resized)
  39. @property (nonatomic, getter = isTransitioning) BOOL transitioning;
  40. // Optional classes to be used instead of the default ones.
  41. @property (nonatomic, strong) Class textViewClass;
  42. @property (nonatomic, strong) Class replyViewClass;
  43. @property (nonatomic, strong) Class typingIndicatorViewClass;
  44. @property (nonatomic, strong) NSNotification *lastKeyboardNotification;
  45. @end
  46. @implementation SLKTextViewController
  47. @synthesize tableView = _tableView;
  48. @synthesize collectionView = _collectionView;
  49. @synthesize scrollView = _scrollView;
  50. @synthesize replyProxyView = _replyProxyView;
  51. @synthesize textInputbar = _textInputbar;
  52. @synthesize autoCompletionView = _autoCompletionView;
  53. @synthesize autoCompleting = _autoCompleting;
  54. @synthesize scrollViewProxy = _scrollViewProxy;
  55. @synthesize presentedInPopover = _presentedInPopover;
  56. #pragma mark - Initializer
  57. - (instancetype)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil
  58. {
  59. return [self initWithTableViewStyle:UITableViewStylePlain];
  60. }
  61. - (instancetype)init
  62. {
  63. return [self initWithTableViewStyle:UITableViewStylePlain];
  64. }
  65. - (instancetype)initWithTableViewStyle:(UITableViewStyle)style
  66. {
  67. NSAssert([self class] != [SLKTextViewController class], @"Oops! You must subclass SLKTextViewController.");
  68. NSAssert(style == UITableViewStylePlain || style == UITableViewStyleGrouped, @"Oops! You must pass a valid UITableViewStyle.");
  69. if (self = [super initWithNibName:nil bundle:nil])
  70. {
  71. self.scrollViewProxy = [self tableViewWithStyle:style];
  72. [self slk_commonInit];
  73. }
  74. return self;
  75. }
  76. - (instancetype)initWithCollectionViewLayout:(UICollectionViewLayout *)layout
  77. {
  78. NSAssert([self class] != [SLKTextViewController class], @"Oops! You must subclass SLKTextViewController.");
  79. NSAssert([layout isKindOfClass:[UICollectionViewLayout class]], @"Oops! You must pass a valid UICollectionViewLayout object.");
  80. if (self = [super initWithNibName:nil bundle:nil])
  81. {
  82. self.scrollViewProxy = [self collectionViewWithLayout:layout];
  83. [self slk_commonInit];
  84. }
  85. return self;
  86. }
  87. - (instancetype)initWithScrollView:(UIScrollView *)scrollView
  88. {
  89. NSAssert([self class] != [SLKTextViewController class], @"Oops! You must subclass SLKTextViewController.");
  90. NSAssert([scrollView isKindOfClass:[UIScrollView class]], @"Oops! You must pass a valid UIScrollView object.");
  91. if (self = [super initWithNibName:nil bundle:nil])
  92. {
  93. _scrollView = scrollView;
  94. _scrollView.translatesAutoresizingMaskIntoConstraints = NO; // Makes sure the scrollView plays nice with auto-layout
  95. self.scrollViewProxy = _scrollView;
  96. [self slk_commonInit];
  97. }
  98. return self;
  99. }
  100. - (instancetype)initWithCoder:(NSCoder *)decoder
  101. {
  102. NSAssert([self class] != [SLKTextViewController class], @"Oops! You must subclass SLKTextViewController.");
  103. NSAssert([decoder isKindOfClass:[NSCoder class]], @"Oops! You must pass a valid decoder object.");
  104. if (self = [super initWithCoder:decoder])
  105. {
  106. UITableViewStyle tableViewStyle = [[self class] tableViewStyleForCoder:decoder];
  107. UICollectionViewLayout *collectionViewLayout = [[self class] collectionViewLayoutForCoder:decoder];
  108. if ([collectionViewLayout isKindOfClass:[UICollectionViewLayout class]]) {
  109. self.scrollViewProxy = [self collectionViewWithLayout:collectionViewLayout];
  110. }
  111. else {
  112. self.scrollViewProxy = [self tableViewWithStyle:tableViewStyle];
  113. }
  114. [self slk_commonInit];
  115. }
  116. return self;
  117. }
  118. - (void)slk_commonInit
  119. {
  120. [self slk_registerNotifications];
  121. self.bounces = YES;
  122. self.inverted = YES;
  123. self.shakeToClearEnabled = NO;
  124. self.shouldClearTextAtRightButtonPress = YES;
  125. self.shouldScrollToBottomAfterKeyboardShows = NO;
  126. self.automaticallyAdjustsScrollViewInsets = YES;
  127. self.extendedLayoutIncludesOpaqueBars = YES;
  128. }
  129. #pragma mark - View lifecycle
  130. - (void)loadView
  131. {
  132. [super loadView];
  133. }
  134. - (void)viewDidLoad
  135. {
  136. [super viewDidLoad];
  137. [self.view addSubview:self.scrollViewProxy];
  138. [self.view addSubview:self.autoCompletionView];
  139. [self.view addSubview:self.replyProxyView];
  140. [self.view addSubview:self.textInputbar];
  141. [self slk_setupViewConstraints];
  142. [self slk_registerKeyCommands];
  143. }
  144. - (void)viewWillAppear:(BOOL)animated
  145. {
  146. [super viewWillAppear:animated];
  147. // Invalidates this flag when the view appears
  148. self.textView.didNotResignFirstResponder = NO;
  149. // Forces laying out the recently added subviews and update their constraints
  150. [self.view layoutIfNeeded];
  151. [UIView performWithoutAnimation:^{
  152. // Reloads any cached text
  153. [self slk_reloadTextView];
  154. }];
  155. }
  156. - (void)viewDidAppear:(BOOL)animated
  157. {
  158. [super viewDidAppear:animated];
  159. [self.scrollViewProxy flashScrollIndicators];
  160. self.viewVisible = YES;
  161. }
  162. - (void)viewWillDisappear:(BOOL)animated
  163. {
  164. [super viewWillDisappear:animated];
  165. // Stops the keyboard from being dismissed during the navigation controller's "swipe-to-pop"
  166. self.textView.didNotResignFirstResponder = self.isMovingFromParentViewController;
  167. self.viewVisible = NO;
  168. }
  169. - (void)viewDidDisappear:(BOOL)animated
  170. {
  171. [super viewDidDisappear:animated];
  172. // Caches the text before it's too late!
  173. [self cacheTextView];
  174. }
  175. - (void)viewWillLayoutSubviews
  176. {
  177. [super viewWillLayoutSubviews];
  178. [self slk_adjustContentConfigurationIfNeeded];
  179. }
  180. - (void)viewDidLayoutSubviews
  181. {
  182. [super viewDidLayoutSubviews];
  183. // Make sure that the background view of textInputBar (UIToolBar)
  184. // covers the safe area bottom gap.
  185. if (@available(iOS 11.0, *)) {
  186. UIView *textInputBackgroudView = nil;
  187. CGRect textInputContentFrame = CGRectZero;
  188. for (UIView *subview in self.textInputbar.subviews) {
  189. if ([NSStringFromClass([subview class]) containsString:@"UIBarBackground"]) {
  190. textInputBackgroudView = subview;
  191. if (@available(iOS 13.0, *)) {
  192. // Workaround for iOS 13+ since UIToolbarContentView subview doesn't exist any more.
  193. // We just need to set to textInputBackgroudView a different frame than the current one, so the view is redrawn.
  194. textInputContentFrame = textInputBackgroudView.frame;
  195. }
  196. }
  197. if ([NSStringFromClass([subview class]) containsString:@"UIToolbarContentView"]) {
  198. textInputContentFrame = subview.frame;
  199. }
  200. }
  201. if (textInputBackgroudView && textInputContentFrame.size.height > 0) {
  202. CGRect textInputBackgroudViewFrame = textInputBackgroudView.frame;
  203. textInputBackgroudViewFrame.size.height = textInputContentFrame.size.height + self.view.safeAreaInsets.bottom;
  204. [textInputBackgroudView setFrame: textInputBackgroudViewFrame];
  205. }
  206. }
  207. }
  208. - (void)viewSafeAreaInsetsDidChange
  209. {
  210. [super viewSafeAreaInsetsDidChange];
  211. [self slk_updateViewConstraints];
  212. }
  213. #pragma mark - Getters
  214. + (UITableViewStyle)tableViewStyleForCoder:(NSCoder *)decoder
  215. {
  216. return UITableViewStylePlain;
  217. }
  218. + (UICollectionViewLayout *)collectionViewLayoutForCoder:(NSCoder *)decoder
  219. {
  220. return nil;
  221. }
  222. - (UITableView *)tableViewWithStyle:(UITableViewStyle)style
  223. {
  224. if (!_tableView) {
  225. _tableView = [[UITableView alloc] initWithFrame:CGRectZero style:style];
  226. _tableView.translatesAutoresizingMaskIntoConstraints = NO;
  227. _tableView.scrollsToTop = YES;
  228. _tableView.dataSource = self;
  229. _tableView.delegate = self;
  230. _tableView.clipsToBounds = NO;
  231. [self slk_updateInsetAdjustmentBehavior];
  232. }
  233. return _tableView;
  234. }
  235. - (UICollectionView *)collectionViewWithLayout:(UICollectionViewLayout *)layout
  236. {
  237. if (!_collectionView) {
  238. _collectionView = [[UICollectionView alloc] initWithFrame:CGRectZero collectionViewLayout:layout];
  239. _collectionView.backgroundColor = [UIColor whiteColor];
  240. _collectionView.translatesAutoresizingMaskIntoConstraints = NO;
  241. _collectionView.scrollsToTop = YES;
  242. _collectionView.dataSource = self;
  243. _collectionView.delegate = self;
  244. }
  245. return _collectionView;
  246. }
  247. - (UITableView *)autoCompletionView
  248. {
  249. if (!_autoCompletionView) {
  250. _autoCompletionView = [[UITableView alloc] initWithFrame:CGRectZero style:UITableViewStylePlain];
  251. _autoCompletionView.translatesAutoresizingMaskIntoConstraints = NO;
  252. _autoCompletionView.backgroundColor = [UIColor colorWithWhite:0.97 alpha:1.0];
  253. _autoCompletionView.scrollsToTop = NO;
  254. _autoCompletionView.dataSource = self;
  255. _autoCompletionView.delegate = self;
  256. #ifdef __IPHONE_9_0
  257. if ([_autoCompletionView respondsToSelector:@selector(cellLayoutMarginsFollowReadableWidth)]) {
  258. _autoCompletionView.cellLayoutMarginsFollowReadableWidth = NO;
  259. }
  260. #endif
  261. CGRect rect = CGRectZero;
  262. rect.size = CGSizeMake(CGRectGetWidth(self.view.frame), 0.5);
  263. _autoCompletionHairline = [[UIView alloc] initWithFrame:rect];
  264. _autoCompletionHairline.autoresizingMask = UIViewAutoresizingFlexibleWidth;
  265. _autoCompletionHairline.backgroundColor = _autoCompletionView.separatorColor;
  266. [_autoCompletionView addSubview:_autoCompletionHairline];
  267. }
  268. return _autoCompletionView;
  269. }
  270. - (SLKTextInputbar *)textInputbar
  271. {
  272. if (!_textInputbar) {
  273. if (_typingIndicatorViewClass != nil) {
  274. _textInputbar = [[SLKTextInputbar alloc] initWithTextViewClass:self.textViewClass withTypingIndicatorViewClass:self.typingIndicatorViewClass];
  275. } else {
  276. _textInputbar = [[SLKTextInputbar alloc] initWithTextViewClass:self.textViewClass];
  277. }
  278. _textInputbar.translatesAutoresizingMaskIntoConstraints = NO;
  279. [_textInputbar.leftButton addTarget:self action:@selector(didPressLeftButton:) forControlEvents:UIControlEventTouchUpInside];
  280. [_textInputbar.rightButton addTarget:self action:@selector(didPressRightButton:) forControlEvents:UIControlEventTouchUpInside];
  281. [_textInputbar.editorLeftButton addTarget:self action:@selector(didCancelTextEditing:) forControlEvents:UIControlEventTouchUpInside];
  282. [_textInputbar.editorRightButton addTarget:self action:@selector(didCommitTextEditing:) forControlEvents:UIControlEventTouchUpInside];
  283. _textInputbar.textView.delegate = self;
  284. }
  285. return _textInputbar;
  286. }
  287. - (UIView <SLKVisibleViewProtocol> *)replyProxyView
  288. {
  289. if (!_replyProxyView) {
  290. if (self.replyViewClass == nil) {
  291. _replyProxyView = [[SLKDefaultReplyView alloc] init];
  292. } else {
  293. Class class = self.replyViewClass;
  294. _replyProxyView = [[class alloc] init];
  295. _replyProxyView.translatesAutoresizingMaskIntoConstraints = NO;
  296. _replyProxyView.hidden = YES;
  297. [_replyProxyView addObserver:self forKeyPath:@"visible" options:NSKeyValueObservingOptionNew context:nil];
  298. }
  299. }
  300. return _replyProxyView;
  301. }
  302. - (BOOL)isPresentedInPopover
  303. {
  304. return _presentedInPopover && SLK_IS_IPAD;
  305. }
  306. - (BOOL)isTextInputbarHidden
  307. {
  308. return _textInputbar.hidden;
  309. }
  310. - (SLKTextView *)textView
  311. {
  312. return _textInputbar.textView;
  313. }
  314. - (UIButton *)leftButton
  315. {
  316. return _textInputbar.leftButton;
  317. }
  318. - (UIButton *)rightButton
  319. {
  320. return _textInputbar.rightButton;
  321. }
  322. - (UIModalPresentationStyle)modalPresentationStyle
  323. {
  324. if (self.navigationController) {
  325. return self.navigationController.modalPresentationStyle;
  326. }
  327. return [super modalPresentationStyle];
  328. }
  329. - (CGFloat)slk_appropriateKeyboardHeightFromNotification:(NSNotification *)notification
  330. {
  331. // Let's first detect keyboard special states such as external keyboard, undocked or split layouts.
  332. [self slk_detectKeyboardStatesInNotification:notification];
  333. if ([self ignoreTextInputbarAdjustment]) {
  334. return [self slk_appropriateBottomMargin];
  335. }
  336. CGRect keyboardRect = [notification.userInfo[UIKeyboardFrameEndUserInfoKey] CGRectValue];
  337. return [self slk_appropriateKeyboardHeightFromRect:keyboardRect];
  338. }
  339. - (CGFloat)slk_appropriateKeyboardHeightFromRect:(CGRect)rect
  340. {
  341. CGRect keyboardRect = [self.view convertRect:rect fromView:nil];
  342. CGFloat viewHeight = CGRectGetHeight(self.view.bounds);
  343. CGFloat keyboardMinY = CGRectGetMinY(keyboardRect);
  344. // Find out how the view is positioned on screen. When in slide over mode, we need
  345. // to take the y-position additionally into account to correctly position the view
  346. UIView *baseView = self.view.window.rootViewController.view;
  347. // In case our view is opened on a modal, we need to take the position of our view in the baseView (relative to the window) into account
  348. CGRect frameOnBaseView = [baseView convertRect:self.view.frame toView:self.view.window];
  349. CGRect frameOnScreen = [baseView convertRect:baseView.frame toCoordinateSpace:[UIScreen mainScreen].coordinateSpace];
  350. CGFloat yPositionOnScreen = MAX(0.0, CGRectGetMinY(frameOnScreen) - CGRectGetMinY(frameOnBaseView));
  351. CGFloat keyboardHeight = MAX(0.0, viewHeight - keyboardMinY + yPositionOnScreen);
  352. CGFloat bottomMargin = [self slk_appropriateBottomMargin];
  353. // When the keyboard height is zero, we can assume there is no keyboard visible
  354. // In that case, let's see if there are any other views outside of the view hiearchy
  355. // requiring to adjust the text input bottom margin
  356. if (keyboardHeight < bottomMargin) {
  357. keyboardHeight = bottomMargin;
  358. }
  359. return keyboardHeight;
  360. }
  361. - (CGFloat)slk_appropriateBottomMargin
  362. {
  363. // A bottom margin is required if the view is extended out of it bounds
  364. if ((self.edgesForExtendedLayout & UIRectEdgeBottom) > 0) {
  365. UITabBar *tabBar = self.tabBarController.tabBar;
  366. // Considers the bottom tab bar, unless it will be hidden
  367. if (tabBar && !tabBar.hidden && !self.hidesBottomBarWhenPushed) {
  368. return CGRectGetHeight(tabBar.frame);
  369. }
  370. }
  371. // A bottom margin is required for iPhone X
  372. if (@available(iOS 11.0, *)) {
  373. return self.view.safeAreaInsets.bottom;
  374. }
  375. return 0.0;
  376. }
  377. - (CGFloat)slk_appropriateScrollViewHeight
  378. {
  379. CGFloat scrollViewHeight = CGRectGetHeight(self.view.bounds);
  380. scrollViewHeight -= self.keyboardHC.constant;
  381. scrollViewHeight -= self.textInputbarHC.constant;
  382. scrollViewHeight -= self.autoCompletionViewHC.constant;
  383. scrollViewHeight -= self.replyViewHC.constant;
  384. if (scrollViewHeight < 0) return 0;
  385. else return scrollViewHeight;
  386. }
  387. - (CGFloat)slk_topBarsHeight
  388. {
  389. // No need to adjust if the edge isn't available
  390. if ((self.edgesForExtendedLayout & UIRectEdgeTop) == 0) {
  391. return 0.0;
  392. }
  393. CGFloat topBarsHeight = CGRectGetHeight(self.navigationController.navigationBar.frame);
  394. if ((SLK_IS_IPHONE && SLK_IS_LANDSCAPE && SLK_IS_IOS8_AND_HIGHER) ||
  395. (SLK_IS_IPAD && self.modalPresentationStyle == UIModalPresentationFormSheet) ||
  396. self.isPresentedInPopover) {
  397. return topBarsHeight;
  398. }
  399. #ifndef APP_EXTENSION
  400. topBarsHeight += CGRectGetHeight([UIApplication sharedApplication].statusBarFrame);
  401. #endif
  402. return topBarsHeight;
  403. }
  404. - (NSString *)slk_appropriateKeyboardNotificationName:(NSNotification *)notification
  405. {
  406. NSString *name = notification.name;
  407. if ([name isEqualToString:UIKeyboardWillShowNotification]) {
  408. return SLKKeyboardWillShowNotification;
  409. }
  410. if ([name isEqualToString:UIKeyboardWillHideNotification]) {
  411. return SLKKeyboardWillHideNotification;
  412. }
  413. if ([name isEqualToString:UIKeyboardDidShowNotification]) {
  414. return SLKKeyboardDidShowNotification;
  415. }
  416. if ([name isEqualToString:UIKeyboardDidHideNotification]) {
  417. return SLKKeyboardDidHideNotification;
  418. }
  419. return nil;
  420. }
  421. - (SLKKeyboardStatus)slk_keyboardStatusForNotification:(NSNotification *)notification
  422. {
  423. NSString *name = notification.name;
  424. if ([name isEqualToString:UIKeyboardWillShowNotification]) {
  425. return SLKKeyboardStatusWillShow;
  426. }
  427. if ([name isEqualToString:UIKeyboardDidShowNotification]) {
  428. return SLKKeyboardStatusDidShow;
  429. }
  430. if ([name isEqualToString:UIKeyboardWillHideNotification]) {
  431. return SLKKeyboardStatusWillHide;
  432. }
  433. if ([name isEqualToString:UIKeyboardDidHideNotification]) {
  434. return SLKKeyboardStatusDidHide;
  435. }
  436. return -1;
  437. }
  438. - (BOOL)slk_isIllogicalKeyboardStatus:(SLKKeyboardStatus)newStatus
  439. {
  440. if ((self.keyboardStatus == SLKKeyboardStatusDidHide && newStatus == SLKKeyboardStatusWillShow) ||
  441. (self.keyboardStatus == SLKKeyboardStatusWillShow && newStatus == SLKKeyboardStatusDidShow) ||
  442. (self.keyboardStatus == SLKKeyboardStatusDidShow && newStatus == SLKKeyboardStatusWillHide) ||
  443. (self.keyboardStatus == SLKKeyboardStatusWillHide && newStatus == SLKKeyboardStatusDidHide)) {
  444. return NO;
  445. }
  446. return YES;
  447. }
  448. #pragma mark - Setters
  449. - (void)setEdgesForExtendedLayout:(UIRectEdge)rectEdge
  450. {
  451. if (self.edgesForExtendedLayout == rectEdge) {
  452. return;
  453. }
  454. [super setEdgesForExtendedLayout:rectEdge];
  455. [self slk_updateViewConstraints];
  456. }
  457. - (void)setScrollViewProxy:(UIScrollView *)scrollView
  458. {
  459. if ([_scrollViewProxy isEqual:scrollView]) {
  460. return;
  461. }
  462. _singleTapGesture = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(slk_didTapScrollView:)];
  463. _singleTapGesture.delegate = self;
  464. [_singleTapGesture requireGestureRecognizerToFail:scrollView.panGestureRecognizer];
  465. [scrollView addGestureRecognizer:self.singleTapGesture];
  466. _scrollViewProxy = scrollView;
  467. }
  468. - (void)setAutoCompleting:(BOOL)autoCompleting
  469. {
  470. if (_autoCompleting == autoCompleting) {
  471. return;
  472. }
  473. _autoCompleting = autoCompleting;
  474. self.scrollViewProxy.scrollEnabled = !autoCompleting;
  475. }
  476. - (void)setInverted:(BOOL)inverted
  477. {
  478. if (_inverted == inverted) {
  479. return;
  480. }
  481. _inverted = inverted;
  482. [self slk_updateInsetAdjustmentBehavior];
  483. self.scrollViewProxy.transform = inverted ? CGAffineTransformMake(1, 0, 0, -1, 0, 0) : CGAffineTransformIdentity;
  484. }
  485. - (void)setBounces:(BOOL)bounces
  486. {
  487. _bounces = bounces;
  488. _textInputbar.bounces = bounces;
  489. }
  490. - (void)slk_updateInsetAdjustmentBehavior
  491. {
  492. // Deactivate automatic scrollView adjustment for inverted table view
  493. if (@available(iOS 11.0, *)) {
  494. if (self.isInverted) {
  495. _tableView.contentInsetAdjustmentBehavior = UIScrollViewContentInsetAdjustmentNever;
  496. } else {
  497. _tableView.contentInsetAdjustmentBehavior = UIScrollViewContentInsetAdjustmentAutomatic;
  498. }
  499. }
  500. }
  501. - (BOOL)slk_updateKeyboardStatus:(SLKKeyboardStatus)status
  502. {
  503. // Skips if trying to update the same status
  504. if (_keyboardStatus == status) {
  505. return NO;
  506. }
  507. // Skips illogical conditions
  508. // Forces the keyboard status when didHide to avoid any inconsistency.
  509. if (status != SLKKeyboardStatusDidHide && [self slk_isIllogicalKeyboardStatus:status]) {
  510. return NO;
  511. }
  512. _keyboardStatus = status;
  513. [self didChangeKeyboardStatus:status];
  514. return YES;
  515. }
  516. #pragma mark - Public & Subclassable Methods
  517. - (void)presentKeyboard:(BOOL)animated
  518. {
  519. // Skips if already first responder
  520. if ([self.textView isFirstResponder]) {
  521. return;
  522. }
  523. if (!animated) {
  524. [UIView performWithoutAnimation:^{
  525. [self.textView becomeFirstResponder];
  526. }];
  527. }
  528. else {
  529. [self.textView becomeFirstResponder];
  530. }
  531. }
  532. - (void)dismissKeyboard:(BOOL)animated
  533. {
  534. // Dismisses the keyboard from any first responder in the window.
  535. if (![self.textView isFirstResponder] && self.keyboardHC.constant > 0) {
  536. [self.view.window endEditing:NO];
  537. }
  538. if (!animated) {
  539. [UIView performWithoutAnimation:^{
  540. [self.textView resignFirstResponder];
  541. }];
  542. }
  543. else {
  544. [self.textView resignFirstResponder];
  545. }
  546. }
  547. - (BOOL)forceTextInputbarAdjustmentForResponder:(UIResponder *)responder
  548. {
  549. return NO;
  550. }
  551. - (BOOL)ignoreTextInputbarAdjustment
  552. {
  553. if (self.isExternalKeyboardDetected || self.isKeyboardUndocked) {
  554. return YES;
  555. }
  556. return NO;
  557. }
  558. - (void)didChangeKeyboardStatus:(SLKKeyboardStatus)status
  559. {
  560. // No implementation here. Meant to be overriden in subclass.
  561. }
  562. - (void)textWillUpdate
  563. {
  564. // No implementation here. Meant to be overriden in subclass.
  565. }
  566. - (void)textDidUpdate:(BOOL)animated
  567. {
  568. if (self.isTextInputbarHidden) {
  569. return;
  570. }
  571. [_textInputbar layoutIfNeeded];
  572. CGFloat inputbarHeight = _textInputbar.appropriateHeight;
  573. _textInputbar.rightButton.enabled = [self canPressRightButton];
  574. _textInputbar.editorRightButton.enabled = [self canPressRightButton];
  575. if (inputbarHeight != self.textInputbarHC.constant)
  576. {
  577. CGFloat inputBarHeightDelta = inputbarHeight - self.textInputbarHC.constant;
  578. CGPoint newOffset = CGPointMake(0, self.scrollViewProxy.contentOffset.y + inputBarHeightDelta);
  579. self.textInputbarHC.constant = inputbarHeight;
  580. self.scrollViewHC.constant = [self slk_appropriateScrollViewHeight];
  581. if (animated) {
  582. BOOL bounces = self.bounces && [self.textView isFirstResponder];
  583. __weak typeof(self) weakSelf = self;
  584. [self.view slk_animateLayoutIfNeededWithBounce:bounces
  585. options:UIViewAnimationOptionCurveEaseInOut|UIViewAnimationOptionLayoutSubviews|UIViewAnimationOptionBeginFromCurrentState
  586. animations:^{
  587. if (!self.isInverted) {
  588. self.scrollViewProxy.contentOffset = newOffset;
  589. }
  590. if (weakSelf.textInputbar.isEditing) {
  591. [weakSelf.textView slk_scrollToCaretPositonAnimated:NO];
  592. }
  593. }];
  594. }
  595. else {
  596. [self.view layoutIfNeeded];
  597. }
  598. }
  599. // Toggles auto-correction if requiered
  600. [self slk_enableTypingSuggestionIfNeeded];
  601. }
  602. - (void)textSelectionDidChange
  603. {
  604. // The text view must be first responder
  605. if (![self.textView isFirstResponder] || self.keyboardStatus != SLKKeyboardStatusDidShow) {
  606. return;
  607. }
  608. // Skips there is a real text selection
  609. if (self.textView.isTrackpadEnabled) {
  610. return;
  611. }
  612. if (self.textView.selectedRange.length > 0) {
  613. if (self.isAutoCompleting && [self shouldProcessTextForAutoCompletion]) {
  614. [self cancelAutoCompletion];
  615. }
  616. return;
  617. }
  618. // Process the text at every caret movement
  619. [self slk_processTextForAutoCompletion];
  620. }
  621. - (BOOL)canPressRightButton
  622. {
  623. NSString *text = [self.textView.text stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]];
  624. if (text.length > 0 && ![_textInputbar limitExceeded]) {
  625. return YES;
  626. }
  627. return NO;
  628. }
  629. - (void)didPressLeftButton:(id)sender
  630. {
  631. // No implementation here. Meant to be overriden in subclass.
  632. }
  633. - (void)didPressRightButton:(id)sender
  634. {
  635. if (self.shouldClearTextAtRightButtonPress) {
  636. // Clears the text and the undo manager
  637. [self.textView slk_clearText:YES];
  638. }
  639. // Clears cache
  640. [self clearCachedText];
  641. }
  642. - (void)editText:(NSString *)text
  643. {
  644. NSAttributedString *attributedText = [self.textView slk_defaultAttributedStringForText:text];
  645. [self editAttributedText:attributedText];
  646. }
  647. - (void)editAttributedText:(NSAttributedString *)attributedText
  648. {
  649. if (![_textInputbar canEditText:attributedText.string]) {
  650. return;
  651. }
  652. // Caches the current text, in case the user cancels the edition
  653. [self slk_cacheAttributedTextToDisk:self.textView.attributedText];
  654. [_textInputbar beginTextEditing];
  655. // Setting the text after calling -beginTextEditing is safer
  656. [self.textView setAttributedText:attributedText];
  657. [self.textView slk_scrollToCaretPositonAnimated:YES];
  658. // Brings up the keyboard if needed
  659. [self presentKeyboard:YES];
  660. }
  661. - (void)didCommitTextEditing:(id)sender
  662. {
  663. if (!_textInputbar.isEditing) {
  664. return;
  665. }
  666. [_textInputbar endTextEdition];
  667. // Clears the text and but not the undo manager
  668. [self.textView slk_clearText:NO];
  669. }
  670. - (void)didCancelTextEditing:(id)sender
  671. {
  672. if (!_textInputbar.isEditing) {
  673. return;
  674. }
  675. [_textInputbar endTextEdition];
  676. // Clears the text and but not the undo manager
  677. [self.textView slk_clearText:NO];
  678. // Restores any previous cached text before entering in editing mode
  679. [self slk_reloadTextView];
  680. }
  681. - (BOOL)canShowReplyView
  682. {
  683. // Don't show if the text is being edited or auto-completed.
  684. if (_textInputbar.isEditing || self.isAutoCompleting) {
  685. return NO;
  686. }
  687. return YES;
  688. }
  689. - (CGFloat)heightForAutoCompletionView
  690. {
  691. return 0.0;
  692. }
  693. - (CGFloat)maximumHeightForAutoCompletionView
  694. {
  695. CGFloat maxiumumHeight = SLKAutoCompletionViewDefaultHeight;
  696. if (self.isAutoCompleting) {
  697. CGFloat scrollViewHeight = self.scrollViewHC.constant;
  698. scrollViewHeight -= [self slk_topBarsHeight];
  699. if (scrollViewHeight < maxiumumHeight) {
  700. maxiumumHeight = scrollViewHeight;
  701. }
  702. }
  703. return maxiumumHeight;
  704. }
  705. - (void)didPasteMediaContent:(NSDictionary *)userInfo
  706. {
  707. // No implementation here. Meant to be overriden in subclass.
  708. }
  709. - (void)willRequestUndo
  710. {
  711. NSString *title = NSLocalizedString(@"Undo Typing", nil);
  712. NSString *acceptTitle = NSLocalizedString(@"Undo", nil);
  713. NSString *cancelTitle = NSLocalizedString(@"Cancel", nil);
  714. #ifdef __IPHONE_8_0
  715. UIAlertController *alertController = [UIAlertController alertControllerWithTitle:title message:nil preferredStyle:UIAlertControllerStyleAlert];
  716. [alertController addAction:[UIAlertAction actionWithTitle:acceptTitle style:UIAlertActionStyleDefault handler:^(UIAlertAction *action) {
  717. // Clears the text but doesn't clear the undo manager
  718. if (self.shakeToClearEnabled) {
  719. [self.textView slk_clearText:NO];
  720. }
  721. }]];
  722. [alertController addAction:[UIAlertAction actionWithTitle:cancelTitle style:UIAlertActionStyleCancel handler:NULL]];
  723. [self presentViewController:alertController animated:YES completion:nil];
  724. #else
  725. UIAlertView *alert = [UIAlertView new];
  726. [alert setTitle:title];
  727. [alert addButtonWithTitle:acceptTitle];
  728. [alert addButtonWithTitle:cancelTitle];
  729. [alert setCancelButtonIndex:1];
  730. [alert setTag:kSLKAlertViewClearTextTag];
  731. [alert setDelegate:self];
  732. [alert show];
  733. #endif
  734. }
  735. - (void)setTextInputbarHidden:(BOOL)hidden
  736. {
  737. [self setTextInputbarHidden:hidden animated:NO];
  738. }
  739. - (void)setTextInputbarHidden:(BOOL)hidden animated:(BOOL)animated
  740. {
  741. if (self.isTextInputbarHidden == hidden) {
  742. return;
  743. }
  744. _textInputbar.hidden = hidden;
  745. if (@available(iOS 11.0, *)) {
  746. [self viewSafeAreaInsetsDidChange];
  747. }
  748. __weak typeof(self) weakSelf = self;
  749. void (^animations)(void) = ^void(){
  750. weakSelf.textInputbarHC.constant = hidden ? 0.0 : weakSelf.textInputbar.appropriateHeight;
  751. [weakSelf.view layoutIfNeeded];
  752. };
  753. void (^completion)(BOOL finished) = ^void(BOOL finished){
  754. if (hidden) {
  755. [self dismissKeyboard:YES];
  756. }
  757. };
  758. if (animated) {
  759. [UIView animateWithDuration:0.25 animations:animations completion:completion];
  760. }
  761. else {
  762. animations();
  763. completion(NO);
  764. }
  765. }
  766. #pragma mark - Private Methods
  767. - (void)slk_didTapScrollView:(UIGestureRecognizer *)gesture
  768. {
  769. if (!self.isPresentedInPopover && ![self ignoreTextInputbarAdjustment]) {
  770. [self dismissKeyboard:YES];
  771. }
  772. }
  773. - (void)slk_performRightAction
  774. {
  775. NSArray *actions = [self.rightButton actionsForTarget:self forControlEvent:UIControlEventTouchUpInside];
  776. if (actions.count > 0 && [self canPressRightButton]) {
  777. [self.rightButton sendActionsForControlEvents:UIControlEventTouchUpInside];
  778. }
  779. }
  780. - (void)slk_postKeyboarStatusNotification:(NSNotification *)notification
  781. {
  782. if ([self ignoreTextInputbarAdjustment] || self.isTransitioning) {
  783. return;
  784. }
  785. NSMutableDictionary *userInfo = [notification.userInfo mutableCopy];
  786. CGRect beginFrame = [notification.userInfo[UIKeyboardFrameBeginUserInfoKey] CGRectValue];
  787. CGRect endFrame = [notification.userInfo[UIKeyboardFrameEndUserInfoKey] CGRectValue];
  788. // Fixes iOS7 oddness with inverted values on landscape orientation
  789. if (!SLK_IS_IOS8_AND_HIGHER && SLK_IS_LANDSCAPE) {
  790. beginFrame = SLKRectInvert(beginFrame);
  791. endFrame = SLKRectInvert(endFrame);
  792. }
  793. CGFloat keyboardHeight = CGRectGetHeight(endFrame);
  794. beginFrame.size.height = keyboardHeight;
  795. endFrame.size.height = keyboardHeight;
  796. [userInfo setObject:[NSValue valueWithCGRect:beginFrame] forKey:UIKeyboardFrameBeginUserInfoKey];
  797. [userInfo setObject:[NSValue valueWithCGRect:endFrame] forKey:UIKeyboardFrameEndUserInfoKey];
  798. NSString *name = [self slk_appropriateKeyboardNotificationName:notification];
  799. [[NSNotificationCenter defaultCenter] postNotificationName:name object:self.textView userInfo:userInfo];
  800. }
  801. - (void)slk_enableTypingSuggestionIfNeeded
  802. {
  803. if (![self.textView isFirstResponder]) {
  804. return;
  805. }
  806. BOOL enable = !self.isAutoCompleting;
  807. NSString *inputPrimaryLanguage = self.textView.textInputMode.primaryLanguage;
  808. // Toggling autocorrect on Japanese keyboards breaks autocompletion by replacing the autocompletion prefix by an empty string.
  809. // So for now, let's not disable autocorrection for Japanese.
  810. if ([inputPrimaryLanguage isEqualToString:@"ja-JP"]) {
  811. return;
  812. }
  813. // Let's avoid refreshing the text view while dictation mode is enabled.
  814. // This solves a crash some users were experiencing when auto-completing with the dictation input mode.
  815. if ([inputPrimaryLanguage isEqualToString:@"dictation"]) {
  816. return;
  817. }
  818. if (enable == NO && ![self shouldDisableTypingSuggestionForAutoCompletion]) {
  819. return;
  820. }
  821. [self.textView setTypingSuggestionEnabled:enable];
  822. }
  823. - (void)slk_dismissTextInputbarIfNeeded
  824. {
  825. CGFloat bottomMargin = [self slk_appropriateBottomMargin];
  826. if (self.keyboardHC.constant == bottomMargin) {
  827. return;
  828. }
  829. self.keyboardHC.constant = bottomMargin;
  830. self.scrollViewHC.constant = [self slk_appropriateScrollViewHeight];
  831. [self slk_hideAutoCompletionViewIfNeeded];
  832. [self.view layoutIfNeeded];
  833. }
  834. - (void)slk_detectKeyboardStatesInNotification:(NSNotification *)notification
  835. {
  836. // Tear down
  837. _externalKeyboardDetected = NO;
  838. _keyboardUndocked = NO;
  839. if (self.isMovingKeyboard) {
  840. return;
  841. }
  842. // Based on http://stackoverflow.com/a/5760910/287403
  843. // We can determine if the external keyboard is showing by adding the origin.y of the target finish rect (end when showing, begin when hiding) to the inputAccessoryHeight.
  844. // If it's greater(or equal) the window height, it's an external keyboard.
  845. CGRect beginRect = [notification.userInfo[UIKeyboardFrameBeginUserInfoKey] CGRectValue];
  846. CGRect endRect = [notification.userInfo[UIKeyboardFrameEndUserInfoKey] CGRectValue];
  847. // Grab the base view for conversions as we don't want window coordinates in < iOS 8
  848. // iOS 8 fixes the whole coordinate system issue for us, but iOS 7 doesn't rotate the app window coordinate space.
  849. UIView *baseView = self.view.window.rootViewController.view;
  850. CGRect screenBounds = [UIScreen mainScreen].bounds;
  851. // Convert the main screen bounds into the correct coordinate space but ignore the origin.
  852. CGRect viewBounds = [self.view convertRect:self.view.window.bounds fromView:nil];
  853. viewBounds = CGRectMake(0, 0, viewBounds.size.width, viewBounds.size.height);
  854. // We want these rects in the correct coordinate space as well.
  855. CGRect convertBegin = [baseView convertRect:beginRect fromView:nil];
  856. CGRect convertEnd = [baseView convertRect:endRect fromView:nil];
  857. if ([notification.name isEqualToString:UIKeyboardWillShowNotification]) {
  858. if (convertEnd.origin.y >= viewBounds.size.height) {
  859. _externalKeyboardDetected = YES;
  860. }
  861. }
  862. else if ([notification.name isEqualToString:UIKeyboardWillHideNotification]) {
  863. // The additional logic check here (== to width) accounts for a glitch (iOS 8 only?) where the window has rotated it's coordinates
  864. // but the beginRect doesn't yet reflect that. It should never cause a false positive.
  865. if (convertBegin.origin.y >= viewBounds.size.height ||
  866. convertBegin.origin.y == viewBounds.size.width) {
  867. _externalKeyboardDetected = YES;
  868. }
  869. }
  870. // Find out how the view is positioned on screen. When in slide over mode, we need
  871. // to take the y-position additionally into account to correctly detect undocked keyboards
  872. CGRect frameOnScreen = [baseView convertRect:baseView.frame toCoordinateSpace:[UIScreen mainScreen].coordinateSpace];
  873. CGFloat yPositionOnScreen = MAX(0.0, CGRectGetMinY(frameOnScreen));
  874. if (SLK_IS_IPAD && (CGRectGetMaxY(convertEnd) + yPositionOnScreen) < CGRectGetMaxY(screenBounds)) {
  875. // The keyboard is undocked or split (iPad Only)
  876. _keyboardUndocked = YES;
  877. // An external keyboard cannot be detected anymore
  878. _externalKeyboardDetected = NO;
  879. }
  880. }
  881. - (void)slk_adjustContentConfigurationIfNeeded
  882. {
  883. UIEdgeInsets contentInset = self.scrollViewProxy.contentInset;
  884. // When inverted, we need to substract the top bars height (generally status bar + navigation bar's) to align the top of the
  885. // scrollView correctly to its top edge.
  886. if (self.inverted) {
  887. contentInset.bottom = [self slk_topBarsHeight];
  888. contentInset.top = contentInset.bottom > 0.0 ? 0.0 : contentInset.top;
  889. }
  890. else {
  891. contentInset.bottom = 0.0;
  892. }
  893. self.scrollViewProxy.contentInset = contentInset;
  894. self.scrollViewProxy.scrollIndicatorInsets = contentInset;
  895. }
  896. - (void)slk_prepareForInterfaceTransitionWithDuration:(NSTimeInterval)duration
  897. {
  898. self.transitioning = YES;
  899. [self.view layoutIfNeeded];
  900. if ([self.textView isFirstResponder]) {
  901. [self.textView slk_scrollToCaretPositonAnimated:NO];
  902. }
  903. else {
  904. [self.textView slk_scrollToBottomAnimated:NO];
  905. }
  906. NSIndexPath *lastVisibleRowIndexPath = [[self.tableView indexPathsForVisibleRows] lastObject];
  907. // Disables the flag after the rotation animation is finished
  908. // Hacky but works.
  909. dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(duration * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
  910. self.transitioning = NO;
  911. if (lastVisibleRowIndexPath) {
  912. [self.tableView scrollToRowAtIndexPath:lastVisibleRowIndexPath atScrollPosition:UITableViewScrollPositionBottom animated:NO];
  913. }
  914. [self.textView setNeedsLayout];
  915. [self.textView layoutIfNeeded];
  916. });
  917. }
  918. #pragma mark - Keyboard Events
  919. - (void)didPressReturnKey:(UIKeyCommand *)keyCommand
  920. {
  921. if (_textInputbar.isEditing) {
  922. [self didCommitTextEditing:keyCommand];
  923. }
  924. else {
  925. [self slk_performRightAction];
  926. }
  927. }
  928. - (void)didPressEscapeKey:(UIKeyCommand *)keyCommand
  929. {
  930. if (self.isAutoCompleting) {
  931. [self cancelAutoCompletion];
  932. }
  933. else if (_textInputbar.isEditing) {
  934. [self didCancelTextEditing:keyCommand];
  935. }
  936. CGFloat bottomMargin = [self slk_appropriateBottomMargin];
  937. if ([self ignoreTextInputbarAdjustment] || ([self.textView isFirstResponder] && self.keyboardHC.constant == bottomMargin)) {
  938. return;
  939. }
  940. [self dismissKeyboard:YES];
  941. }
  942. - (void)didPressArrowKey:(UIKeyCommand *)keyCommand
  943. {
  944. [self.textView didPressArrowKey:keyCommand];
  945. }
  946. #pragma mark - Notification Events
  947. - (void)slk_willShowOrHideKeyboard:(NSNotification *)notification
  948. {
  949. SLKKeyboardStatus status = [self slk_keyboardStatusForNotification:notification];
  950. // Skips if the view isn't visible.
  951. if (!self.isViewVisible) {
  952. return;
  953. }
  954. // Skips if it is presented inside of a popover.
  955. if (self.isPresentedInPopover) {
  956. return;
  957. }
  958. // Skips if textview did refresh only.
  959. if (self.textView.didNotResignFirstResponder) {
  960. return;
  961. }
  962. UIResponder *currentResponder = [UIResponder slk_currentFirstResponder];
  963. // Skips if it's not the expected textView and shouldn't force adjustment of the text input bar.
  964. // This will also dismiss the text input bar if it's visible, and exit auto-completion mode if enabled.
  965. if (currentResponder && ![currentResponder isEqual:self.textView] && ![self forceTextInputbarAdjustmentForResponder:currentResponder]) {
  966. [self slk_dismissTextInputbarIfNeeded];
  967. return;
  968. }
  969. // Skips if it's the current status
  970. if (self.keyboardStatus == status) {
  971. return;
  972. }
  973. // Programatically stops scrolling before updating the view constraints (to avoid scrolling glitch).
  974. if (status == SLKKeyboardStatusWillShow) {
  975. [self.scrollViewProxy slk_stopScrolling];
  976. }
  977. // Stores the previous keyboard height
  978. CGFloat previousKeyboardHeight = self.keyboardHC.constant;
  979. // Updates the height constraints' constants
  980. self.keyboardHC.constant = [self slk_appropriateKeyboardHeightFromNotification:notification];
  981. self.scrollViewHC.constant = [self slk_appropriateScrollViewHeight];
  982. // Updates and notifies about the keyboard status update
  983. if ([self slk_updateKeyboardStatus:status]) {
  984. // Posts custom keyboard notification, if logical conditions apply
  985. [self slk_postKeyboarStatusNotification:notification];
  986. }
  987. // Hides the auto-completion view if the keyboard is being dismissed.
  988. if (![self.textView isFirstResponder] || status == SLKKeyboardStatusWillHide) {
  989. [self slk_hideAutoCompletionViewIfNeeded];
  990. }
  991. UIScrollView *scrollView = self.scrollViewProxy;
  992. NSInteger curve = [notification.userInfo[UIKeyboardAnimationCurveUserInfoKey] integerValue];
  993. NSTimeInterval duration = [notification.userInfo[UIKeyboardAnimationDurationUserInfoKey] doubleValue];
  994. CGRect beginFrame = [notification.userInfo[UIKeyboardFrameBeginUserInfoKey] CGRectValue];
  995. CGRect endFrame = [notification.userInfo[UIKeyboardFrameEndUserInfoKey] CGRectValue];
  996. void (^animations)(void) = ^void() {
  997. // Scrolls to bottom only if the keyboard is about to show.
  998. if (self.shouldScrollToBottomAfterKeyboardShows && self.keyboardStatus == SLKKeyboardStatusWillShow) {
  999. if (self.isInverted) {
  1000. [scrollView slk_scrollToTopAnimated:YES];
  1001. }
  1002. else {
  1003. [scrollView slk_scrollToBottomAnimated:YES];
  1004. }
  1005. }
  1006. };
  1007. // Begin and end frames are the same when the keyboard is shown during navigation controller's push animation.
  1008. // The animation happens in window coordinates (slides from right to left) but doesn't in the view controller's view coordinates.
  1009. // Second condition: check if the height of the keyboard changed.
  1010. if (!CGRectEqualToRect(beginFrame, endFrame) || fabs(previousKeyboardHeight - self.keyboardHC.constant) > 0.0)
  1011. {
  1012. // Content Offset correction if not inverted and not auto-completing.
  1013. if (!self.isInverted && !self.isAutoCompleting) {
  1014. CGFloat scrollViewHeight = self.scrollViewHC.constant;
  1015. CGFloat keyboardHeight = self.keyboardHC.constant;
  1016. CGSize contentSize = scrollView.contentSize;
  1017. CGPoint contentOffset = scrollView.contentOffset;
  1018. CGFloat newOffset = MIN(contentSize.height - scrollViewHeight,
  1019. contentOffset.y + keyboardHeight - previousKeyboardHeight);
  1020. scrollView.contentOffset = CGPointMake(contentOffset.x, newOffset);
  1021. }
  1022. if (duration == 0) {
  1023. [self.view layoutIfNeeded];
  1024. } else {
  1025. // Only for this animation, we set bo to bounce since we want to give the impression that the text input is glued to the keyboard.
  1026. self.lastKeyboardNotification = notification;
  1027. CADisplayLink *displayLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(adjustKeyboardWhileAnimating)];
  1028. [displayLink addToRunLoop:[NSRunLoop currentRunLoop] forMode:NSRunLoopCommonModes];
  1029. [CATransaction begin];
  1030. [CATransaction setCompletionBlock:^{
  1031. [displayLink invalidate];
  1032. }];
  1033. [self.view slk_animateLayoutIfNeededWithDuration:duration
  1034. bounce:NO
  1035. options:(curve<<16)|UIViewAnimationOptionLayoutSubviews|UIViewAnimationOptionBeginFromCurrentState
  1036. animations:animations
  1037. completion:NULL];
  1038. [CATransaction commit];
  1039. }
  1040. }
  1041. else {
  1042. animations();
  1043. }
  1044. }
  1045. - (void)adjustKeyboardWhileAnimating
  1046. {
  1047. // We check the keyboard height while we are doing the keyboard animation
  1048. // This fixes the positioning of the keyboard when displayed on an iPad as a form sheet
  1049. CGFloat previousKeyboardHeight = self.keyboardHC.constant;
  1050. CGFloat newKeyboardHeight = [self slk_appropriateKeyboardHeightFromNotification:self.lastKeyboardNotification];
  1051. if (previousKeyboardHeight != newKeyboardHeight) {
  1052. // Updates the height constraints' constants
  1053. self.keyboardHC.constant = newKeyboardHeight;
  1054. self.scrollViewHC.constant = [self slk_appropriateScrollViewHeight];
  1055. }
  1056. }
  1057. - (void)slk_didShowOrHideKeyboard:(NSNotification *)notification
  1058. {
  1059. SLKKeyboardStatus status = [self slk_keyboardStatusForNotification:notification];
  1060. // Skips if the view isn't visible
  1061. if (!self.isViewVisible) {
  1062. if (status == SLKKeyboardStatusDidHide && self.keyboardStatus == SLKKeyboardStatusWillHide) {
  1063. // Even if the view isn't visible anymore, let's still continue to update all states.
  1064. }
  1065. else {
  1066. return;
  1067. }
  1068. }
  1069. // Skips if it is presented inside of a popover
  1070. if (self.isPresentedInPopover) {
  1071. return;
  1072. }
  1073. // Skips if textview did refresh only
  1074. if (self.textView.didNotResignFirstResponder) {
  1075. return;
  1076. }
  1077. // Skips if it's the current status
  1078. if (self.keyboardStatus == status) {
  1079. return;
  1080. }
  1081. // Updates and notifies about the keyboard status update
  1082. if ([self slk_updateKeyboardStatus:status]) {
  1083. // Posts custom keyboard notification, if logical conditions apply
  1084. [self slk_postKeyboarStatusNotification:notification];
  1085. }
  1086. // After showing keyboard, check if the current cursor position could diplay autocompletion
  1087. if ([self.textView isFirstResponder] && status == SLKKeyboardStatusDidShow && !self.isAutoCompleting) {
  1088. // Wait till the end of the current run loop
  1089. dispatch_async(dispatch_get_main_queue(), ^{
  1090. [self slk_processTextForAutoCompletion];
  1091. });
  1092. }
  1093. // Very important to invalidate this flag after the keyboard is dismissed or presented, to start with a clean state next time.
  1094. self.movingKeyboard = NO;
  1095. }
  1096. - (void)slk_didPostSLKKeyboardNotification:(NSNotification *)notification
  1097. {
  1098. if (![notification.object isEqual:self.textView]) {
  1099. return;
  1100. }
  1101. // Used for debug only
  1102. NSLog(@"%@ %s: %@", NSStringFromClass([self class]), __FUNCTION__, notification);
  1103. }
  1104. - (void)slk_willChangeTextViewText:(NSNotification *)notification
  1105. {
  1106. // Skips this it's not the expected textView.
  1107. if (![notification.object isEqual:self.textView]) {
  1108. return;
  1109. }
  1110. [self textWillUpdate];
  1111. }
  1112. - (void)slk_didChangeTextViewText:(NSNotification *)notification
  1113. {
  1114. // Skips this it's not the expected textView.
  1115. if (![notification.object isEqual:self.textView]) {
  1116. return;
  1117. }
  1118. // Animated only if the view already appeared.
  1119. [self textDidUpdate:self.isViewVisible];
  1120. // Process the text at every change, when the view is visible
  1121. if (self.isViewVisible) {
  1122. [self slk_processTextForAutoCompletion];
  1123. }
  1124. }
  1125. - (void)slk_didChangeTextViewContentSize:(NSNotification *)notification
  1126. {
  1127. // Skips this it's not the expected textView.
  1128. if (![notification.object isEqual:self.textView]) {
  1129. return;
  1130. }
  1131. // Animated only if the view already appeared.
  1132. [self textDidUpdate:self.isViewVisible];
  1133. }
  1134. - (void)slk_didChangeInputbarContentSize:(NSNotification *)notification
  1135. {
  1136. // Skips this it's not the expected textView.
  1137. if (![notification.object isEqual:self.textInputbar]) {
  1138. return;
  1139. }
  1140. // Animated only if the view already appeared.
  1141. [self textDidUpdate:self.isViewVisible];
  1142. }
  1143. - (void)slk_didChangeTextViewSelectedRange:(NSNotification *)notification
  1144. {
  1145. // Skips this it's not the expected textView.
  1146. if (![notification.object isEqual:self.textView]) {
  1147. return;
  1148. }
  1149. [self textSelectionDidChange];
  1150. }
  1151. - (void)slk_didChangeTextViewPasteboard:(NSNotification *)notification
  1152. {
  1153. // Skips this if it's not the expected textView.
  1154. if (![self.textView isFirstResponder]) {
  1155. return;
  1156. }
  1157. // Notifies only if the pasted item is nested in a dictionary.
  1158. if (notification.userInfo) {
  1159. [self didPasteMediaContent:notification.userInfo];
  1160. }
  1161. }
  1162. - (void)slk_didShakeTextView:(NSNotification *)notification
  1163. {
  1164. // Skips this if it's not the expected textView.
  1165. if (![self.textView isFirstResponder]) {
  1166. return;
  1167. }
  1168. // Notifies of the shake gesture if undo mode is on and the text view is not empty
  1169. if (self.shakeToClearEnabled && self.textView.text.length > 0) {
  1170. [self willRequestUndo];
  1171. }
  1172. }
  1173. - (void)slk_willShowOrHideTypeIndicatorView:(UIView <SLKVisibleViewProtocol> *)view
  1174. {
  1175. // Skips if the reply view should not show. Ignores the checking if it's trying to hide.
  1176. if (![self canShowReplyView] && view.isVisible) {
  1177. return;
  1178. }
  1179. CGFloat systemLayoutSizeHeight = [view systemLayoutSizeFittingSize:UILayoutFittingCompressedSize].height;
  1180. CGFloat height = view.isVisible ? systemLayoutSizeHeight : 0.0;
  1181. self.replyViewHC.constant = height;
  1182. self.scrollViewHC.constant -= height;
  1183. if (view.isVisible) {
  1184. view.hidden = NO;
  1185. }
  1186. [self.view slk_animateLayoutIfNeededWithBounce:self.bounces
  1187. options:UIViewAnimationOptionCurveEaseInOut
  1188. animations:NULL
  1189. completion:^(BOOL finished) {
  1190. if (!view.isVisible) {
  1191. view.hidden = YES;
  1192. }
  1193. }];
  1194. }
  1195. #pragma mark - KVO Events
  1196. - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
  1197. {
  1198. if ([object conformsToProtocol:@protocol(SLKVisibleViewProtocol)] && [keyPath isEqualToString:@"visible"]) {
  1199. [self slk_willShowOrHideTypeIndicatorView:object];
  1200. }
  1201. else {
  1202. [super observeValueForKeyPath:keyPath ofObject:object change:change context:context];
  1203. }
  1204. }
  1205. #pragma mark - Auto-Completion Text Processing
  1206. - (void)registerPrefixesForAutoCompletion:(NSArray <NSString *> *)prefixes
  1207. {
  1208. if (prefixes.count == 0) {
  1209. return;
  1210. }
  1211. NSMutableSet *set = [NSMutableSet setWithSet:self.registeredPrefixes];
  1212. [set addObjectsFromArray:[prefixes copy]];
  1213. _registeredPrefixes = [NSSet setWithSet:set];
  1214. }
  1215. - (BOOL)shouldProcessTextForAutoCompletion
  1216. {
  1217. if (!_registeredPrefixes || _registeredPrefixes.count == 0) {
  1218. return NO;
  1219. }
  1220. return YES;
  1221. }
  1222. - (BOOL)shouldDisableTypingSuggestionForAutoCompletion
  1223. {
  1224. if (!_registeredPrefixes || _registeredPrefixes.count == 0) {
  1225. return NO;
  1226. }
  1227. return YES;
  1228. }
  1229. - (void)didChangeAutoCompletionPrefix:(NSString *)prefix andWord:(NSString *)word
  1230. {
  1231. // No implementation here. Meant to be overriden in subclass.
  1232. }
  1233. - (void)showAutoCompletionView:(BOOL)show
  1234. {
  1235. // Reloads the tableview before showing/hiding
  1236. if (show) {
  1237. [_autoCompletionView reloadData];
  1238. }
  1239. self.autoCompleting = show;
  1240. // Toggles auto-correction if requiered
  1241. [self slk_enableTypingSuggestionIfNeeded];
  1242. CGFloat viewHeight = show ? [self heightForAutoCompletionView] : 0.0;
  1243. if (self.autoCompletionViewHC.constant == viewHeight) {
  1244. return;
  1245. }
  1246. // If the auto-completion view height is bigger than the maximum height allows, it is reduce to that size. Default 140 pts.
  1247. CGFloat maximumHeight = [self maximumHeightForAutoCompletionView];
  1248. if (viewHeight > maximumHeight) {
  1249. viewHeight = maximumHeight;
  1250. }
  1251. CGFloat contentViewHeight = self.scrollViewHC.constant + self.autoCompletionViewHC.constant;
  1252. // On iPhone, the auto-completion view can't extend beyond the content view height
  1253. if (SLK_IS_IPHONE && viewHeight > contentViewHeight) {
  1254. viewHeight = contentViewHeight;
  1255. }
  1256. self.autoCompletionViewHC.constant = viewHeight;
  1257. [self.view slk_animateLayoutIfNeededWithBounce:self.bounces
  1258. options:UIViewAnimationOptionCurveEaseInOut|UIViewAnimationOptionLayoutSubviews|UIViewAnimationOptionBeginFromCurrentState|UIViewAnimationOptionAllowUserInteraction
  1259. animations:NULL];
  1260. }
  1261. - (void)showAutoCompletionViewWithPrefix:(NSString *)prefix andWord:(NSString *)word prefixRange:(NSRange)prefixRange
  1262. {
  1263. if ([self.registeredPrefixes containsObject:prefix]) {
  1264. _foundPrefix = prefix;
  1265. _foundWord = word;
  1266. _foundPrefixRange = prefixRange;
  1267. [self didChangeAutoCompletionPrefix:self.foundPrefix andWord:self.foundWord];
  1268. [self showAutoCompletionView:YES];
  1269. }
  1270. }
  1271. - (void)acceptAutoCompletionWithString:(NSString *)string
  1272. {
  1273. [self acceptAutoCompletionWithString:string keepPrefix:YES];
  1274. }
  1275. - (void)acceptAutoCompletionWithString:(NSString *)string keepPrefix:(BOOL)keepPrefix
  1276. {
  1277. if (string.length == 0) {
  1278. return;
  1279. }
  1280. NSUInteger location = self.foundPrefixRange.location;
  1281. if (keepPrefix) {
  1282. location += self.foundPrefixRange.length;
  1283. }
  1284. NSUInteger length = self.foundWord.length;
  1285. if (!keepPrefix) {
  1286. length += self.foundPrefixRange.length;
  1287. }
  1288. NSRange range = NSMakeRange(location, length);
  1289. NSRange insertionRange = [self.textView slk_insertText:string inRange:range];
  1290. self.textView.selectedRange = NSMakeRange(insertionRange.location, 0);
  1291. [self.textView slk_scrollToCaretPositonAnimated:NO];
  1292. [self cancelAutoCompletion];
  1293. }
  1294. - (void)cancelAutoCompletion
  1295. {
  1296. [self slk_invalidateAutoCompletion];
  1297. [self slk_hideAutoCompletionViewIfNeeded];
  1298. }
  1299. - (void)slk_processTextForAutoCompletion
  1300. {
  1301. NSString *text = self.textView.text;
  1302. if ((!self.isAutoCompleting && text.length == 0) || self.isTransitioning || ![self shouldProcessTextForAutoCompletion]) {
  1303. return;
  1304. }
  1305. [self.textView lookForPrefixes:self.registeredPrefixes
  1306. completion:^(NSString *prefix, NSString *word, NSRange wordRange) {
  1307. if (prefix.length > 0 && word.length > 0) {
  1308. // Captures the detected symbol prefix
  1309. _foundPrefix = prefix;
  1310. // Removes the found prefix, or not.
  1311. _foundWord = [word substringFromIndex:prefix.length];
  1312. // Used later for replacing the detected range with a new string alias returned in -acceptAutoCompletionWithString:
  1313. _foundPrefixRange = NSMakeRange(wordRange.location, prefix.length);
  1314. [self slk_handleProcessedWord:word wordRange:wordRange];
  1315. }
  1316. else {
  1317. [self cancelAutoCompletion];
  1318. }
  1319. }];
  1320. }
  1321. - (void)slk_handleProcessedWord:(NSString *)word wordRange:(NSRange)wordRange
  1322. {
  1323. // Cancel auto-completion if the cursor is placed before the prefix
  1324. if (self.textView.selectedRange.location <= self.foundPrefixRange.location) {
  1325. return [self cancelAutoCompletion];
  1326. }
  1327. if (self.foundPrefix.length > 0) {
  1328. if (wordRange.length == 0 || wordRange.length != word.length) {
  1329. return [self cancelAutoCompletion];
  1330. }
  1331. if (word.length > 0) {
  1332. // If the prefix is still contained in the word, cancels
  1333. if ([self.foundWord rangeOfString:self.foundPrefix].location != NSNotFound) {
  1334. return [self cancelAutoCompletion];
  1335. }
  1336. }
  1337. else {
  1338. return [self cancelAutoCompletion];
  1339. }
  1340. }
  1341. else {
  1342. return [self cancelAutoCompletion];
  1343. }
  1344. [self didChangeAutoCompletionPrefix:self.foundPrefix andWord:self.foundWord];
  1345. }
  1346. - (void)slk_invalidateAutoCompletion
  1347. {
  1348. _foundPrefix = nil;
  1349. _foundWord = nil;
  1350. _foundPrefixRange = NSMakeRange(0,0);
  1351. [_autoCompletionView setContentOffset:CGPointZero];
  1352. }
  1353. - (void)slk_hideAutoCompletionViewIfNeeded
  1354. {
  1355. if (self.isAutoCompleting) {
  1356. [self showAutoCompletionView:NO];
  1357. }
  1358. }
  1359. #pragma mark - Text Caching
  1360. - (NSString *)keyForTextCaching
  1361. {
  1362. // No implementation here. Meant to be overriden in subclass.
  1363. return nil;
  1364. }
  1365. - (NSString *)slk_keyForPersistency
  1366. {
  1367. NSString *key = [self keyForTextCaching];
  1368. if (key == nil) {
  1369. return nil;
  1370. }
  1371. return [NSString stringWithFormat:@"%@.%@", SLKTextViewControllerDomain, key];
  1372. }
  1373. - (void)slk_reloadTextView
  1374. {
  1375. NSString *key = [self slk_keyForPersistency];
  1376. if (key == nil) {
  1377. return;
  1378. }
  1379. NSAttributedString *cachedAttributedText = [[NSAttributedString alloc] initWithString:@""];
  1380. id obj = [[NSUserDefaults standardUserDefaults] objectForKey:key];
  1381. if (obj) {
  1382. if ([obj isKindOfClass:[NSString class]]) {
  1383. cachedAttributedText = [[NSAttributedString alloc] initWithString:obj];
  1384. }
  1385. else if ([obj isKindOfClass:[NSData class]]) {
  1386. cachedAttributedText = [NSKeyedUnarchiver unarchiveObjectWithData:obj];
  1387. }
  1388. }
  1389. if (self.textView.attributedText.length == 0 || cachedAttributedText.length > 0) {
  1390. self.textView.attributedText = cachedAttributedText;
  1391. }
  1392. }
  1393. - (void)cacheTextView
  1394. {
  1395. [self slk_cacheAttributedTextToDisk:self.textView.attributedText];
  1396. }
  1397. - (void)clearCachedText
  1398. {
  1399. [self slk_cacheAttributedTextToDisk:nil];
  1400. }
  1401. - (void)slk_cacheAttributedTextToDisk:(NSAttributedString *)attributedText
  1402. {
  1403. NSString *key = [self slk_keyForPersistency];
  1404. if (!key || key.length == 0) {
  1405. return;
  1406. }
  1407. NSAttributedString *cachedAttributedText = [[NSAttributedString alloc] initWithString:@""];
  1408. id obj = [[NSUserDefaults standardUserDefaults] objectForKey:key];
  1409. if (obj) {
  1410. if ([obj isKindOfClass:[NSString class]]) {
  1411. cachedAttributedText = [[NSAttributedString alloc] initWithString:obj];
  1412. }
  1413. else if ([obj isKindOfClass:[NSData class]]) {
  1414. cachedAttributedText = [NSKeyedUnarchiver unarchiveObjectWithData:obj];
  1415. }
  1416. }
  1417. // Caches text only if its a valid string and not already cached
  1418. if (attributedText.length > 0 && ![attributedText isEqualToAttributedString:cachedAttributedText]) {
  1419. NSData *data = [NSKeyedArchiver archivedDataWithRootObject:attributedText];
  1420. [[NSUserDefaults standardUserDefaults] setObject:data forKey:key];
  1421. }
  1422. // Clears cache only if it exists
  1423. else if (attributedText.length == 0 && cachedAttributedText.length > 0) {
  1424. [[NSUserDefaults standardUserDefaults] removeObjectForKey:key];
  1425. }
  1426. else {
  1427. // Skips so it doesn't hit 'synchronize' unnecessarily
  1428. return;
  1429. }
  1430. [[NSUserDefaults standardUserDefaults] synchronize];
  1431. }
  1432. - (void)slk_cacheTextToDisk:(NSString *)text
  1433. {
  1434. NSString *key = [self slk_keyForPersistency];
  1435. if (!key || key.length == 0) {
  1436. return;
  1437. }
  1438. NSAttributedString *attributedText = [[NSAttributedString alloc] initWithString:text];
  1439. [self slk_cacheAttributedTextToDisk:attributedText];
  1440. }
  1441. + (void)clearAllCachedText
  1442. {
  1443. NSMutableArray *cachedKeys = [NSMutableArray new];
  1444. for (NSString *key in [[[NSUserDefaults standardUserDefaults] dictionaryRepresentation] allKeys]) {
  1445. if ([key rangeOfString:SLKTextViewControllerDomain].location != NSNotFound) {
  1446. [cachedKeys addObject:key];
  1447. }
  1448. }
  1449. if (cachedKeys.count == 0) {
  1450. return;
  1451. }
  1452. for (NSString *cachedKey in cachedKeys) {
  1453. [[NSUserDefaults standardUserDefaults] removeObjectForKey:cachedKey];
  1454. }
  1455. [[NSUserDefaults standardUserDefaults] synchronize];
  1456. }
  1457. #pragma mark - Customization
  1458. - (void)registerClassForTextView:(Class)aClass
  1459. {
  1460. if (aClass == nil) {
  1461. return;
  1462. }
  1463. NSAssert([aClass isSubclassOfClass:[SLKTextView class]], @"The registered class is invalid, it must be a subclass of SLKTextView.");
  1464. self.textViewClass = aClass;
  1465. }
  1466. - (void)registerClassForReplyView:(Class)aClass
  1467. {
  1468. if (aClass == nil) {
  1469. return;
  1470. }
  1471. NSAssert([aClass isSubclassOfClass:[UIView class]], @"The registered class is invalid, it must be a subclass of UIView.");
  1472. self.replyViewClass = aClass;
  1473. }
  1474. - (void)registerClassForTypingIndicatorView:(Class)aClass
  1475. {
  1476. if (aClass == nil) {
  1477. return;
  1478. }
  1479. NSAssert([aClass isSubclassOfClass:[UIView class]], @"The registered class is invalid, it must be a subclass of SLKTextView.");
  1480. self.typingIndicatorViewClass = aClass;
  1481. }
  1482. #pragma mark - UITextViewDelegate Methods
  1483. - (BOOL)textView:(SLKTextView *)textView shouldChangeTextInRange:(NSRange)range replacementText:(NSString *)text
  1484. {
  1485. if (![textView isKindOfClass:[SLKTextView class]]) {
  1486. return YES;
  1487. }
  1488. BOOL newWordInserted = ([text rangeOfCharacterFromSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]].location != NSNotFound);
  1489. // Records text for undo for every new word
  1490. if (newWordInserted) {
  1491. [textView slk_prepareForUndo:@"Word Change"];
  1492. }
  1493. // Detects double spacebar tapping, to replace the default "." insert with a formatting symbol, if needed.
  1494. if (textView.isFormattingEnabled && range.location > 0 && text.length > 0 &&
  1495. [[NSCharacterSet whitespaceCharacterSet] characterIsMember:[text characterAtIndex:0]] &&
  1496. [[NSCharacterSet whitespaceCharacterSet] characterIsMember:[textView.text characterAtIndex:range.location - 1]]) {
  1497. BOOL shouldChange = YES;
  1498. // Since we are moving 2 characters to the left, we need for to make sure that the string's lenght,
  1499. // before the caret position, is higher than 2.
  1500. if ([textView.text substringToIndex:textView.selectedRange.location].length < 2) {
  1501. return YES;
  1502. }
  1503. NSRange wordRange = range;
  1504. wordRange.location -= 2; // minus the white space added with the double space bar tapping
  1505. if (wordRange.location == NSNotFound) {
  1506. return YES;
  1507. }
  1508. NSArray *symbols = textView.registeredSymbols;
  1509. NSMutableCharacterSet *invalidCharacters = [NSMutableCharacterSet new];
  1510. [invalidCharacters formUnionWithCharacterSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]];
  1511. [invalidCharacters formUnionWithCharacterSet:[NSCharacterSet punctuationCharacterSet]];
  1512. [invalidCharacters removeCharactersInString:[symbols componentsJoinedByString:@""]];
  1513. for (NSString *symbol in symbols) {
  1514. // Detects the closest registered symbol to the caret, from right to left
  1515. NSRange searchRange = NSMakeRange(0, wordRange.location);
  1516. NSRange prefixRange = [textView.text rangeOfString:symbol options:NSBackwardsSearch range:searchRange];
  1517. if (prefixRange.location == NSNotFound) {
  1518. continue;
  1519. }
  1520. NSRange nextCharRange = NSMakeRange(prefixRange.location+1, 1);
  1521. NSString *charAfterSymbol = [textView.text substringWithRange:nextCharRange];
  1522. if (prefixRange.location != NSNotFound && ![invalidCharacters characterIsMember:[charAfterSymbol characterAtIndex:0]]) {
  1523. if ([self textView:textView shouldInsertSuffixForFormattingWithSymbol:symbol prefixRange:prefixRange]) {
  1524. NSRange suffixRange;
  1525. [textView wordAtRange:wordRange rangeInText:&suffixRange];
  1526. // Skip if the detected word already has a suffix
  1527. if ([[textView.text substringWithRange:suffixRange] hasSuffix:symbol]) {
  1528. continue;
  1529. }
  1530. suffixRange.location += suffixRange.length;
  1531. suffixRange.length = 0;
  1532. NSString *lastCharacter = [textView.text substringWithRange:NSMakeRange(suffixRange.location, 1)];
  1533. // Checks if the last character was a line break, so we append the symbol in the next line too
  1534. if ([[NSCharacterSet newlineCharacterSet] characterIsMember:[lastCharacter characterAtIndex:0]]) {
  1535. suffixRange.location += 1;
  1536. }
  1537. [textView slk_insertText:symbol inRange:suffixRange];
  1538. shouldChange = NO;
  1539. // Reset the original cursor location +1 for the new character
  1540. NSRange adjustedCursorPosition = NSMakeRange(range.location + 1, 0);
  1541. textView.selectedRange = adjustedCursorPosition;
  1542. break; // exit
  1543. }
  1544. }
  1545. }
  1546. return shouldChange;
  1547. }
  1548. else if ([text isEqualToString:@"\n"]) {
  1549. //Detected break. Should insert new line break programatically instead.
  1550. [textView slk_insertNewLineBreak];
  1551. return NO;
  1552. }
  1553. else {
  1554. NSDictionary *userInfo = @{@"text": text, @"range": [NSValue valueWithRange:range]};
  1555. [[NSNotificationCenter defaultCenter] postNotificationName:SLKTextViewTextWillChangeNotification object:self.textView userInfo:userInfo];
  1556. return YES;
  1557. }
  1558. }
  1559. - (void)textViewDidChange:(SLKTextView *)textView
  1560. {
  1561. // Keep to avoid unnecessary crashes. Was meant to be overriden in subclass while calling super.
  1562. }
  1563. - (void)textViewDidChangeSelection:(SLKTextView *)textView
  1564. {
  1565. // Keep to avoid unnecessary crashes. Was meant to be overriden in subclass while calling super.
  1566. }
  1567. - (BOOL)textViewShouldBeginEditing:(SLKTextView *)textView
  1568. {
  1569. return YES;
  1570. }
  1571. - (BOOL)textViewShouldEndEditing:(SLKTextView *)textView
  1572. {
  1573. return YES;
  1574. }
  1575. - (void)textViewDidBeginEditing:(SLKTextView *)textView
  1576. {
  1577. // No implementation here. Meant to be overriden in subclass.
  1578. }
  1579. - (void)textViewDidEndEditing:(SLKTextView *)textView
  1580. {
  1581. // No implementation here. Meant to be overriden in subclass.
  1582. }
  1583. #pragma mark - SLKTextViewDelegate Methods
  1584. - (BOOL)textView:(SLKTextView *)textView shouldOfferFormattingForSymbol:(NSString *)symbol
  1585. {
  1586. return YES;
  1587. }
  1588. - (BOOL)textView:(SLKTextView *)textView shouldInsertSuffixForFormattingWithSymbol:(NSString *)symbol prefixRange:(NSRange)prefixRange
  1589. {
  1590. if (prefixRange.location > 0) {
  1591. NSRange previousCharRange = NSMakeRange(prefixRange.location-1, 1);
  1592. NSString *previousCharacter = [self.textView.text substringWithRange:previousCharRange];
  1593. // Only insert a suffix if the character before the prefix was a whitespace or a line break
  1594. if ([previousCharacter rangeOfCharacterFromSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]].location != NSNotFound) {
  1595. return YES;
  1596. }
  1597. else {
  1598. return NO;
  1599. }
  1600. }
  1601. return YES;
  1602. }
  1603. #pragma mark - UITableViewDataSource Methods
  1604. - (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
  1605. {
  1606. return 0;
  1607. }
  1608. - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
  1609. {
  1610. return nil;
  1611. }
  1612. #pragma mark - UICollectionViewDataSource Methods
  1613. - (NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section;
  1614. {
  1615. return 0;
  1616. }
  1617. - (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath
  1618. {
  1619. return nil;
  1620. }
  1621. #pragma mark - UIScrollViewDelegate Methods
  1622. - (BOOL)scrollViewShouldScrollToTop:(UIScrollView *)scrollView
  1623. {
  1624. if (!self.scrollViewProxy.scrollsToTop || self.keyboardStatus == SLKKeyboardStatusWillShow) {
  1625. return NO;
  1626. }
  1627. if (self.isInverted) {
  1628. [self.scrollViewProxy slk_scrollToBottomAnimated:YES];
  1629. return NO;
  1630. }
  1631. else {
  1632. return YES;
  1633. }
  1634. }
  1635. - (void)scrollViewDidEndDragging:(UIScrollView *)scrollView willDecelerate:(BOOL)decelerate
  1636. {
  1637. self.movingKeyboard = NO;
  1638. }
  1639. - (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView
  1640. {
  1641. self.movingKeyboard = NO;
  1642. }
  1643. - (void)scrollViewDidScroll:(UIScrollView *)scrollView
  1644. {
  1645. if ([scrollView isEqual:_autoCompletionView]) {
  1646. CGRect frame = self.autoCompletionHairline.frame;
  1647. frame.origin.y = scrollView.contentOffset.y;
  1648. self.autoCompletionHairline.frame = frame;
  1649. }
  1650. else {
  1651. if (!self.isMovingKeyboard) {
  1652. _scrollViewOffsetBeforeDragging = scrollView.contentOffset;
  1653. _keyboardHeightBeforeDragging = self.keyboardHC.constant;
  1654. }
  1655. }
  1656. }
  1657. #pragma mark - UIGestureRecognizerDelegate Methods
  1658. - (BOOL)gestureRecognizerShouldBegin:(UIGestureRecognizer *)gesture
  1659. {
  1660. if ([gesture isEqual:self.singleTapGesture]) {
  1661. return [self.textView isFirstResponder] && ![self ignoreTextInputbarAdjustment];
  1662. }
  1663. return YES;
  1664. }
  1665. #pragma mark - UIAlertViewDelegate Methods
  1666. #ifndef __IPHONE_8_0
  1667. - (void)alertView:(UIAlertView *)alertView clickedButtonAtIndex:(NSInteger)buttonIndex
  1668. {
  1669. if (alertView.tag != kSLKAlertViewClearTextTag || buttonIndex == [alertView cancelButtonIndex] ) {
  1670. return;
  1671. }
  1672. // Clears the text but doesn't clear the undo manager
  1673. if (self.shakeToClearEnabled) {
  1674. [self.textView slk_clearText:NO];
  1675. }
  1676. }
  1677. #endif
  1678. #pragma mark - View Auto-Layout
  1679. - (void)slk_setupViewConstraints
  1680. {
  1681. NSDictionary *views = @{@"scrollView": self.scrollViewProxy,
  1682. @"autoCompletionView": self.autoCompletionView,
  1683. @"replyProxyView": self.replyProxyView,
  1684. @"textInputbar": self.textInputbar
  1685. };
  1686. [self.view addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"V:|[scrollView(0@750)][replyProxyView(0)]-0@999-[textInputbar(0)]|" options:0 metrics:nil views:views]];
  1687. [self.view addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"V:|-(>=0)-[autoCompletionView(0@750)][replyProxyView]" options:0 metrics:nil views:views]];
  1688. [self.view addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"H:|[scrollView]|" options:0 metrics:nil views:views]];
  1689. [self.view addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"H:|[autoCompletionView]|" options:0 metrics:nil views:views]];
  1690. [self.view addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"H:|[replyProxyView]|" options:0 metrics:nil views:views]];
  1691. [self.view addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"H:|[textInputbar]|" options:0 metrics:nil views:views]];
  1692. self.scrollViewHC = [self.view slk_constraintForAttribute:NSLayoutAttributeHeight firstItem:self.scrollViewProxy secondItem:nil];
  1693. self.autoCompletionViewHC = [self.view slk_constraintForAttribute:NSLayoutAttributeHeight firstItem:self.autoCompletionView secondItem:nil];
  1694. self.replyViewHC = [self.view slk_constraintForAttribute:NSLayoutAttributeHeight firstItem:self.replyProxyView secondItem:nil];
  1695. self.textInputbarHC = [self.view slk_constraintForAttribute:NSLayoutAttributeHeight firstItem:self.textInputbar secondItem:nil];
  1696. self.keyboardHC = [self.view slk_constraintForAttribute:NSLayoutAttributeBottom firstItem:self.view secondItem:self.textInputbar];
  1697. [self slk_updateViewConstraints];
  1698. }
  1699. - (void)slk_updateViewConstraints
  1700. {
  1701. self.textInputbarHC.constant = self.textInputbar.hidden ? 0.0 : self.textInputbar.appropriateHeight;
  1702. self.scrollViewHC.constant = [self slk_appropriateScrollViewHeight];
  1703. self.keyboardHC.constant = [self slk_appropriateKeyboardHeightFromRect:CGRectNull];
  1704. if (_textInputbar.isEditing) {
  1705. self.textInputbarHC.constant += self.textInputbar.editorContentViewHeight;
  1706. }
  1707. [super updateViewConstraints];
  1708. }
  1709. - (void)updateViewToShowOrHideEmojiKeyboard:(CGFloat)height
  1710. {
  1711. // Reset view controller if emoji keyboard is hidding
  1712. if (height == 0) {
  1713. [self slk_updateViewConstraints];
  1714. return;
  1715. }
  1716. self.textInputbarHC.constant = 0.0;
  1717. self.keyboardHC.constant = height;
  1718. self.scrollViewHC.constant = [self slk_appropriateScrollViewHeight];
  1719. [super updateViewConstraints];
  1720. }
  1721. #pragma mark - Keyboard Command registration
  1722. - (void)slk_registerKeyCommands
  1723. {
  1724. __weak typeof(self) weakSelf = self;
  1725. // Enter Key
  1726. [self.textView observeKeyInput:@"\r" modifiers:0 title:NSLocalizedString(@"Send/Accept", nil) completion:^(UIKeyCommand *keyCommand) {
  1727. [weakSelf didPressReturnKey:keyCommand];
  1728. }];
  1729. // Esc Key
  1730. [self.textView observeKeyInput:UIKeyInputEscape modifiers:0 title:NSLocalizedString(@"Dismiss", nil) completion:^(UIKeyCommand *keyCommand) {
  1731. [weakSelf didPressEscapeKey:keyCommand];
  1732. }];
  1733. // Up Arrow
  1734. [self.textView observeKeyInput:UIKeyInputUpArrow modifiers:0 title:nil completion:^(UIKeyCommand *keyCommand) {
  1735. [weakSelf didPressArrowKey:keyCommand];
  1736. }];
  1737. // Down Arrow
  1738. [self.textView observeKeyInput:UIKeyInputDownArrow modifiers:0 title:nil completion:^(UIKeyCommand *keyCommand) {
  1739. [weakSelf didPressArrowKey:keyCommand];
  1740. }];
  1741. }
  1742. - (NSArray *)keyCommands
  1743. {
  1744. // Important to keep this in, for backwards compatibility.
  1745. return @[];
  1746. }
  1747. #pragma mark - NSNotificationCenter registration
  1748. - (void)slk_registerNotifications
  1749. {
  1750. [self slk_unregisterNotifications];
  1751. NSNotificationCenter *notificationCenter = [NSNotificationCenter defaultCenter];
  1752. // Keyboard notifications
  1753. [notificationCenter addObserver:self selector:@selector(slk_willShowOrHideKeyboard:) name:UIKeyboardWillShowNotification object:nil];
  1754. [notificationCenter addObserver:self selector:@selector(slk_willShowOrHideKeyboard:) name:UIKeyboardWillHideNotification object:nil];
  1755. [notificationCenter addObserver:self selector:@selector(slk_didShowOrHideKeyboard:) name:UIKeyboardDidShowNotification object:nil];
  1756. [notificationCenter addObserver:self selector:@selector(slk_didShowOrHideKeyboard:) name:UIKeyboardDidHideNotification object:nil];
  1757. #if SLK_KEYBOARD_NOTIFICATION_DEBUG
  1758. [notificationCenter addObserver:self selector:@selector(slk_didPostSLKKeyboardNotification:) name:SLKKeyboardWillShowNotification object:nil];
  1759. [notificationCenter addObserver:self selector:@selector(slk_didPostSLKKeyboardNotification:) name:SLKKeyboardDidShowNotification object:nil];
  1760. [notificationCenter addObserver:self selector:@selector(slk_didPostSLKKeyboardNotification:) name:SLKKeyboardWillHideNotification object:nil];
  1761. [notificationCenter addObserver:self selector:@selector(slk_didPostSLKKeyboardNotification:) name:SLKKeyboardDidHideNotification object:nil];
  1762. #endif
  1763. // TextView notifications
  1764. [notificationCenter addObserver:self selector:@selector(slk_willChangeTextViewText:) name:SLKTextViewTextWillChangeNotification object:nil];
  1765. [notificationCenter addObserver:self selector:@selector(slk_didChangeTextViewText:) name:UITextViewTextDidChangeNotification object:nil];
  1766. [notificationCenter addObserver:self selector:@selector(slk_didChangeTextViewContentSize:) name:SLKTextViewContentSizeDidChangeNotification object:nil];
  1767. [notificationCenter addObserver:self selector:@selector(slk_didChangeTextViewSelectedRange:) name:SLKTextViewSelectedRangeDidChangeNotification object:nil];
  1768. [notificationCenter addObserver:self selector:@selector(slk_didChangeTextViewPasteboard:) name:SLKTextViewDidPasteItemNotification object:nil];
  1769. [notificationCenter addObserver:self selector:@selector(slk_didShakeTextView:) name:SLKTextViewDidShakeNotification object:nil];
  1770. // Inputbar notifications
  1771. [notificationCenter addObserver:self selector:@selector(slk_didChangeInputbarContentSize:) name:SLKTextInputbarContentSizeDidChangeNotification object:nil];
  1772. // Application notifications
  1773. [notificationCenter addObserver:self selector:@selector(cacheTextView) name:UIApplicationWillTerminateNotification object:nil];
  1774. [notificationCenter addObserver:self selector:@selector(cacheTextView) name:UIApplicationDidEnterBackgroundNotification object:nil];
  1775. [notificationCenter addObserver:self selector:@selector(cacheTextView) name:UIApplicationDidReceiveMemoryWarningNotification object:nil];
  1776. }
  1777. - (void)slk_unregisterNotifications
  1778. {
  1779. NSNotificationCenter *notificationCenter = [NSNotificationCenter defaultCenter];
  1780. // Keyboard notifications
  1781. [notificationCenter removeObserver:self name:UIKeyboardWillShowNotification object:nil];
  1782. [notificationCenter removeObserver:self name:UIKeyboardWillHideNotification object:nil];
  1783. [notificationCenter removeObserver:self name:UIKeyboardDidShowNotification object:nil];
  1784. [notificationCenter removeObserver:self name:UIKeyboardDidHideNotification object:nil];
  1785. #if SLK_KEYBOARD_NOTIFICATION_DEBUG
  1786. [notificationCenter removeObserver:self name:SLKKeyboardWillShowNotification object:nil];
  1787. [notificationCenter removeObserver:self name:SLKKeyboardDidShowNotification object:nil];
  1788. [notificationCenter removeObserver:self name:SLKKeyboardWillHideNotification object:nil];
  1789. [notificationCenter removeObserver:self name:SLKKeyboardDidHideNotification object:nil];
  1790. #endif
  1791. // TextView notifications
  1792. [notificationCenter removeObserver:self name:UITextViewTextDidBeginEditingNotification object:nil];
  1793. [notificationCenter removeObserver:self name:UITextViewTextDidEndEditingNotification object:nil];
  1794. [notificationCenter removeObserver:self name:SLKTextViewTextWillChangeNotification object:nil];
  1795. [notificationCenter removeObserver:self name:UITextViewTextDidChangeNotification object:nil];
  1796. [notificationCenter removeObserver:self name:SLKTextViewContentSizeDidChangeNotification object:nil];
  1797. [notificationCenter removeObserver:self name:SLKTextViewSelectedRangeDidChangeNotification object:nil];
  1798. [notificationCenter removeObserver:self name:SLKTextViewDidPasteItemNotification object:nil];
  1799. [notificationCenter removeObserver:self name:SLKTextViewDidShakeNotification object:nil];
  1800. // Inputbar notifications
  1801. [notificationCenter removeObserver:self name:SLKTextInputbarContentSizeDidChangeNotification object:nil];
  1802. // Application notifications
  1803. [notificationCenter removeObserver:self name:UIApplicationWillTerminateNotification object:nil];
  1804. [notificationCenter removeObserver:self name:UIApplicationDidEnterBackgroundNotification object:nil];
  1805. [notificationCenter removeObserver:self name:UIApplicationDidReceiveMemoryWarningNotification object:nil];
  1806. }
  1807. #pragma mark - View Auto-Rotation
  1808. #ifdef __IPHONE_8_0
  1809. - (void)willTransitionToTraitCollection:(UITraitCollection *)newCollection withTransitionCoordinator:(id <UIViewControllerTransitionCoordinator>)coordinator
  1810. {
  1811. [super willTransitionToTraitCollection:newCollection withTransitionCoordinator:coordinator];
  1812. }
  1813. - (void)viewWillTransitionToSize:(CGSize)size withTransitionCoordinator:(id<UIViewControllerTransitionCoordinator>)coordinator
  1814. {
  1815. [self slk_prepareForInterfaceTransitionWithDuration:coordinator.transitionDuration];
  1816. [super viewWillTransitionToSize:size withTransitionCoordinator:coordinator];
  1817. }
  1818. #else
  1819. - (void)willRotateToInterfaceOrientation:(UIInterfaceOrientation)toInterfaceOrientation duration:(NSTimeInterval)duration
  1820. {
  1821. if ([self respondsToSelector:@selector(viewWillTransitionToSize:withTransitionCoordinator:)]) {
  1822. return;
  1823. }
  1824. [self slk_prepareForInterfaceTransitionWithDuration:duration];
  1825. }
  1826. #endif
  1827. #ifdef __IPHONE_9_0
  1828. - (UIInterfaceOrientationMask)supportedInterfaceOrientations
  1829. #else
  1830. - (NSUInteger)supportedInterfaceOrientations
  1831. #endif
  1832. {
  1833. return UIInterfaceOrientationMaskAll;
  1834. }
  1835. - (BOOL)shouldAutorotate
  1836. {
  1837. return YES;
  1838. }
  1839. #pragma mark - View lifeterm
  1840. - (void)didReceiveMemoryWarning
  1841. {
  1842. [super didReceiveMemoryWarning];
  1843. }
  1844. - (void)dealloc
  1845. {
  1846. [self slk_unregisterNotifications];
  1847. [_replyProxyView removeObserver:self forKeyPath:@"visible"];
  1848. }
  1849. @end