You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 

2557 lines
86 KiB

//
// SlackTextViewController
// https://github.com/slackhq/SlackTextViewController
//
// Copyright 2014-2016 Slack Technologies, Inc.
// Licence: MIT-Licence
//
#import "SLKTextViewController.h"
#import "SLKInputAccessoryView.h"
#import "UIResponder+SLKAdditions.h"
#import "SLKUIConstants.h"
#import "KeyboardService.h"
/** Feature flagged while waiting to implement a more reliable technique. */
#define SLKBottomPanningEnabled 0
#define kSLKAlertViewClearTextTag [NSStringFromClass([SLKTextViewController class]) hash]
NSString * const SLKKeyboardWillShowNotification = @"SLKKeyboardWillShowNotification";
NSString * const SLKKeyboardDidShowNotification = @"SLKKeyboardDidShowNotification";
NSString * const SLKKeyboardWillHideNotification = @"SLKKeyboardWillHideNotification";
NSString * const SLKKeyboardDidHideNotification = @"SLKKeyboardDidHideNotification";
CGFloat const SLKAutoCompletionViewDefaultHeight = 140.0;
@interface SLKTextViewController ()
{
CGPoint _scrollViewOffsetBeforeDragging;
CGFloat _keyboardHeightBeforeDragging;
}
// The shared scrollView pointer, either a tableView or collectionView
@property (nonatomic, weak) UIScrollView *scrollViewProxy;
// A hairline displayed on top of the auto-completion view, to better separate the content from the control.
@property (nonatomic, strong) UIView *autoCompletionHairline;
@property (nonatomic, strong) UIView *autoCompletionBackgroundView;
@property (nonatomic, strong) UITapGestureRecognizer *backgroundTapGesture;
// Auto-Layout height constraints used for updating their constants
@property (nonatomic, strong) NSLayoutConstraint *scrollViewHC;
@property (nonatomic, strong) NSLayoutConstraint *textInputbarHC;
@property (nonatomic, strong) NSLayoutConstraint *typingIndicatorViewHC;
@property (nonatomic, strong) NSLayoutConstraint *autoCompletionViewHC;
@property (nonatomic, strong) NSLayoutConstraint *keyboardHC;
@property (nonatomic, assign) CGFloat bottomMargin;
@property (nonatomic, assign) CGFloat contentYOffset;
// YES if the user is moving the keyboard with a gesture
@property (nonatomic, assign, getter = isMovingKeyboard) BOOL movingKeyboard;
// YES if the view controller did appear and everything is finished configurating. This allows blocking some layout animations among other things.
@property (nonatomic, getter=isViewVisible) BOOL viewVisible;
// YES if the view controller's view's size is changing by its parent (i.e. when its window rotates or is resized)
@property (nonatomic, getter = isTransitioning) BOOL transitioning;
// Optional classes to be used instead of the default ones.
@property (nonatomic, strong) Class textViewClass;
@property (nonatomic, strong) Class typingIndicatorViewClass;
@end
@implementation SLKTextViewController
@synthesize tableView = _tableView;
@synthesize collectionView = _collectionView;
@synthesize scrollView = _scrollView;
@synthesize typingIndicatorProxyView = _typingIndicatorProxyView;
@synthesize textInputbar = _textInputbar;
@synthesize autoCompletionView = _autoCompletionView;
@synthesize autoCompleting = _autoCompleting;
@synthesize scrollViewProxy = _scrollViewProxy;
@synthesize presentedInPopover = _presentedInPopover;
@synthesize autoCompletionBackgroundView = _autoCompletionBackgroundView;
@synthesize menuAccesoryView = _menuAccesoryView;
@synthesize autoCompletionHairColor = _autoCompletionHairColor;
#pragma mark - Initializer
- (instancetype)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil
{
return [self initWithTableViewStyle:UITableViewStylePlain];
}
- (instancetype)init
{
return [self initWithTableViewStyle:UITableViewStylePlain];
}
- (instancetype)initWithTableViewStyle:(UITableViewStyle)style
{
NSAssert([self class] != [SLKTextViewController class], @"Oops! You must subclass SLKTextViewController.");
NSAssert(style == UITableViewStylePlain || style == UITableViewStyleGrouped, @"Oops! You must pass a valid UITableViewStyle.");
if (self = [super initWithNibName:nil bundle:nil])
{
self.scrollViewProxy = [self tableViewWithStyle:style];
[self slk_commonInit];
}
return self;
}
- (instancetype)initWithCollectionViewLayout:(UICollectionViewLayout *)layout
{
NSAssert([self class] != [SLKTextViewController class], @"Oops! You must subclass SLKTextViewController.");
NSAssert([layout isKindOfClass:[UICollectionViewLayout class]], @"Oops! You must pass a valid UICollectionViewLayout object.");
if (self = [super initWithNibName:nil bundle:nil])
{
self.scrollViewProxy = [self collectionViewWithLayout:layout];
[self slk_commonInit];
}
return self;
}
- (instancetype)initWithScrollView:(UIScrollView *)scrollView
{
NSAssert([self class] != [SLKTextViewController class], @"Oops! You must subclass SLKTextViewController.");
NSAssert([scrollView isKindOfClass:[UIScrollView class]], @"Oops! You must pass a valid UIScrollView object.");
if (self = [super initWithNibName:nil bundle:nil])
{
_scrollView = scrollView;
_scrollView.translatesAutoresizingMaskIntoConstraints = NO; // Makes sure the scrollView plays nice with auto-layout
self.scrollViewProxy = _scrollView;
[self slk_commonInit];
}
return self;
}
- (instancetype)initWithCoder:(NSCoder *)decoder
{
NSAssert([self class] != [SLKTextViewController class], @"Oops! You must subclass SLKTextViewController.");
NSAssert([decoder isKindOfClass:[NSCoder class]], @"Oops! You must pass a valid decoder object.");
if (self = [super initWithCoder:decoder])
{
UITableViewStyle tableViewStyle = [[self class] tableViewStyleForCoder:decoder];
UICollectionViewLayout *collectionViewLayout = [[self class] collectionViewLayoutForCoder:decoder];
if ([collectionViewLayout isKindOfClass:[UICollectionViewLayout class]]) {
self.scrollViewProxy = [self collectionViewWithLayout:collectionViewLayout];
}
else {
self.scrollViewProxy = [self tableViewWithStyle:tableViewStyle];
}
[self slk_commonInit];
}
return self;
}
- (void)slk_commonInit
{
[self slk_registerNotifications];
self.bounces = YES;
self.inverted = YES;
self.shakeToClearEnabled = NO;
self.keyboardPanningEnabled = YES;
self.shouldClearTextAtRightButtonPress = YES;
self.shouldScrollToBottomAfterKeyboardShows = NO;
self.alwaysEnableRightButton = NO;
self.automaticallyAdjustsScrollViewInsets = YES;
self.extendedLayoutIncludesOpaqueBars = YES;
self.textInputBarLRC = 0;
self.textInputBarBC = 0;
}
#pragma mark - View lifecycle
- (void)loadView
{
[super loadView];
}
- (void)viewDidLoad
{
[super viewDidLoad];
[self.view addSubview:self.scrollViewProxy];
[self.view addSubview:self.autoCompletionView];
[self.view addSubview:self.autoCompletionBackgroundView];
[self.view addSubview:self.typingIndicatorProxyView];
[self.view addSubview:self.textInputbar];
[self slk_setupViewConstraints];
[self slk_registerKeyCommands];
}
- (void)viewWillAppear:(BOOL)animated
{
[super viewWillAppear:animated];
// Invalidates this flag when the view appears
self.textView.didNotResignFirstResponder = NO;
// Forces laying out the recently added subviews and update their constraints
[self.view layoutIfNeeded];
[UIView performWithoutAnimation:^{
// Reloads any cached text
[self slk_reloadTextView];
}];
}
- (void)viewDidAppear:(BOOL)animated
{
[super viewDidAppear:animated];
[self.scrollViewProxy flashScrollIndicators];
self.viewVisible = YES;
}
- (void)viewWillDisappear:(BOOL)animated
{
[super viewWillDisappear:animated];
// Stops the keyboard from being dismissed during the navigation controller's "swipe-to-pop"
self.textView.didNotResignFirstResponder = self.isMovingFromParentViewController;
self.viewVisible = NO;
}
- (void)viewDidDisappear:(BOOL)animated
{
[super viewDidDisappear:animated];
// Caches the text before it's too late!
[self cacheTextView];
}
- (void)viewWillLayoutSubviews
{
[super viewWillLayoutSubviews];
[self slk_adjustContentConfigurationIfNeeded];
}
- (void)viewDidLayoutSubviews
{
[super viewDidLayoutSubviews];
}
- (void)viewSafeAreaInsetsDidChange
{
[super viewSafeAreaInsetsDidChange];
[self slk_updateViewConstraints];
}
#pragma mark - Getters
+ (UITableViewStyle)tableViewStyleForCoder:(NSCoder *)decoder
{
return UITableViewStylePlain;
}
+ (UICollectionViewLayout *)collectionViewLayoutForCoder:(NSCoder *)decoder
{
return nil;
}
- (UITableView *)tableViewWithStyle:(UITableViewStyle)style
{
if (!_tableView) {
_tableView = [[UITableView alloc] initWithFrame:CGRectZero style:style];
_tableView.translatesAutoresizingMaskIntoConstraints = NO;
_tableView.scrollsToTop = YES;
_tableView.dataSource = self;
_tableView.delegate = self;
_tableView.clipsToBounds = NO;
}
return _tableView;
}
- (UICollectionView *)collectionViewWithLayout:(UICollectionViewLayout *)layout
{
if (!_collectionView) {
_collectionView = [[UICollectionView alloc] initWithFrame:CGRectZero collectionViewLayout:layout];
_collectionView.backgroundColor = [UIColor whiteColor];
_collectionView.translatesAutoresizingMaskIntoConstraints = NO;
_collectionView.scrollsToTop = YES;
_collectionView.dataSource = self;
_collectionView.delegate = self;
}
return _collectionView;
}
- (UITableView *)autoCompletionView
{
if (!_autoCompletionView) {
_autoCompletionView = [[UITableView alloc] initWithFrame:CGRectZero style:UITableViewStylePlain];
_autoCompletionView.translatesAutoresizingMaskIntoConstraints = NO;
_autoCompletionView.backgroundColor = [UIColor colorWithWhite:0.97 alpha:1.0];
_autoCompletionView.scrollsToTop = NO;
_autoCompletionView.dataSource = self;
_autoCompletionView.delegate = self;
#ifdef __IPHONE_9_0
if ([_autoCompletionView respondsToSelector:@selector(cellLayoutMarginsFollowReadableWidth)]) {
_autoCompletionView.cellLayoutMarginsFollowReadableWidth = NO;
}
#endif
CGRect rect = CGRectZero;
rect.size = CGSizeMake(CGRectGetWidth(self.view.frame), 0.5);
_autoCompletionHairline = [[UIView alloc] initWithFrame:rect];
_autoCompletionHairline.autoresizingMask = UIViewAutoresizingFlexibleWidth;
_autoCompletionHairline.backgroundColor = _autoCompletionView.separatorColor;
[_autoCompletionView addSubview:_autoCompletionHairline];
}
return _autoCompletionView;
}
- (UIView *)autoCompletionBackgroundView
{
if (!_autoCompletionBackgroundView) {
_autoCompletionBackgroundView = [[UIView alloc] initWithFrame:CGRectZero];
_autoCompletionBackgroundView.translatesAutoresizingMaskIntoConstraints = NO;
_autoCompletionBackgroundView.backgroundColor = [UIColor blackColor];
_autoCompletionBackgroundView.alpha = 0.3;
_autoCompletionBackgroundView.userInteractionEnabled = YES;
_backgroundTapGesture = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(slk_didTapScrollView:)];
_backgroundTapGesture.numberOfTapsRequired = 1;
_backgroundTapGesture.delegate = self;
[_autoCompletionBackgroundView addGestureRecognizer:self.backgroundTapGesture];
[_autoCompletionBackgroundView setHidden:YES];
}
return _autoCompletionBackgroundView;
}
- (void)setAutoCompletionHairColor:(UIColor *)autoCompletionHairColor
{
if (_autoCompletionHairline) {
_autoCompletionHairline.backgroundColor = autoCompletionHairColor;
}
_autoCompletionHairColor = autoCompletionHairColor;
}
- (SLKTextInputbar *)textInputbar
{
if (!_textInputbar) {
_textInputbar = [[SLKTextInputbar alloc] initWithTextViewClass:self.textViewClass];
_textInputbar.translatesAutoresizingMaskIntoConstraints = NO;
[_textInputbar.leftButton addTarget:self action:@selector(didPressLeftButton:) forControlEvents:UIControlEventTouchUpInside];
[_textInputbar.rightButton addTarget:self action:@selector(didPressRightButton:) forControlEvents:UIControlEventTouchUpInside];
[_textInputbar.editorLeftButton addTarget:self action:@selector(didCancelTextEditing:) forControlEvents:UIControlEventTouchUpInside];
[_textInputbar.editorRightButton addTarget:self action:@selector(didCommitTextEditing:) forControlEvents:UIControlEventTouchUpInside];
_textInputbar.textView.delegate = self;
_verticalPanGesture = [[UIPanGestureRecognizer alloc] initWithTarget:self action:@selector(slk_didPanTextInputBar:)];
_verticalPanGesture.delegate = self;
[_textInputbar addGestureRecognizer:self.verticalPanGesture];
}
return _textInputbar;
}
- (UIView <SLKTypingIndicatorProtocol> *)typingIndicatorProxyView
{
if (!_typingIndicatorProxyView) {
Class class = self.typingIndicatorViewClass ? : [SLKTypingIndicatorView class];
_typingIndicatorProxyView = [[class alloc] init];
_typingIndicatorProxyView.translatesAutoresizingMaskIntoConstraints = NO;
_typingIndicatorProxyView.hidden = YES;
[_typingIndicatorProxyView addObserver:self forKeyPath:@"visible" options:NSKeyValueObservingOptionNew context:nil];
}
return _typingIndicatorProxyView;
}
- (SLKTypingIndicatorView *)typingIndicatorView
{
if ([_typingIndicatorProxyView isKindOfClass:[SLKTypingIndicatorView class]]) {
return (SLKTypingIndicatorView *)self.typingIndicatorProxyView;
}
return nil;
}
- (BOOL)isPresentedInPopover
{
return _presentedInPopover && SLK_IS_IPAD;
}
- (BOOL)isTextInputbarHidden
{
return _textInputbar.hidden;
}
- (SLKTextView *)textView
{
return _textInputbar.textView;
}
- (UIButton *)leftButton
{
return _textInputbar.leftButton;
}
- (UIButton *)rightButton
{
return _textInputbar.rightButton;
}
- (UIModalPresentationStyle)modalPresentationStyle
{
if (self.navigationController) {
return self.navigationController.modalPresentationStyle;
}
return [super modalPresentationStyle];
}
- (CGFloat)slk_appropriateKeyboardHeightFromNotification:(NSNotification *)notification
{
// Let's first detect keyboard special states such as external keyboard, undocked or split layouts.
[self slk_detectKeyboardStatesInNotification:notification];
if ([self ignoreTextInputbarAdjustment]) {
return [self slk_appropriateBottomMargin];
}
CGRect keyboardRect = [notification.userInfo[UIKeyboardFrameEndUserInfoKey] CGRectValue];
return [self slk_appropriateKeyboardHeightFromRect:keyboardRect];
}
- (CGFloat)slk_appropriateKeyboardHeightFromRect:(CGRect)rect
{
CGRect keyboardRect = [self.view convertRect:rect fromView:nil];
CGFloat viewHeight = CGRectGetHeight(self.view.bounds);
CGFloat keyboardMinY = CGRectGetMinY(keyboardRect);
CGFloat keyboardHeight = MAX(0.0, viewHeight - keyboardMinY);
CGFloat bottomMargin = [self slk_appropriateBottomMargin];
// When the keyboard height is zero, we can assume there is no keyboard visible
// In that case, let's see if there are any other views outside of the view hiearchy
// requiring to adjust the text input bottom margin
if (keyboardHeight < bottomMargin) {
keyboardHeight = bottomMargin;
} else {
keyboardHeight += self.textInputBarBC;
}
return keyboardHeight;
}
- (CGFloat)slk_appropriateBottomMargin
{
// A bottom margin is required only if the view is extended out of it bounds
if ((self.edgesForExtendedLayout & UIRectEdgeBottom) > 0) {
UITabBar *tabBar = self.tabBarController.tabBar;
// Considers the bottom tab bar, unless it will be hidden
if (tabBar && !tabBar.hidden && !self.hidesBottomBarWhenPushed) {
return CGRectGetHeight(tabBar.frame);
}
}
CGFloat padding = [[KeyboardService shared] keyboardHeight] + 5.f;
if (@available(iOS 11.0, *)) {
padding -= [[[UIApplication sharedApplication] keyWindow] safeAreaInsets].bottom;
}
CGFloat height = self.menuAccesoryView == nil ? self.textInputBarBC : padding;
// A bottom margin is required for iPhone X
if (@available(iOS 11.0, *)) {
return self.view.safeAreaInsets.bottom + height + _bottomMargin;
}
return height + _bottomMargin;
}
- (CGFloat)slk_appropriateScrollViewHeight
{
CGFloat scrollViewHeight = CGRectGetHeight(self.view.bounds);
scrollViewHeight -= self.keyboardHC.constant;
scrollViewHeight -= self.textInputbarHC.constant;
scrollViewHeight -= self.autoCompletionViewHC.constant;
scrollViewHeight -= self.typingIndicatorViewHC.constant;
if (scrollViewHeight < 0) return 0;
else return scrollViewHeight;
}
- (CGFloat)slk_topBarsHeight
{
// No need to adjust if the edge isn't available
if ((self.edgesForExtendedLayout & UIRectEdgeTop) == 0) {
return 0.0;
}
CGFloat topBarsHeight = CGRectGetHeight(self.navigationController.navigationBar.frame);
if ((SLK_IS_IPHONE && SLK_IS_LANDSCAPE && SLK_IS_IOS8_AND_HIGHER) ||
(SLK_IS_IPAD && self.modalPresentationStyle == UIModalPresentationFormSheet) ||
self.isPresentedInPopover) {
return topBarsHeight;
}
topBarsHeight += CGRectGetHeight([UIApplication sharedApplication].statusBarFrame);
return topBarsHeight;
}
- (NSString *)slk_appropriateKeyboardNotificationName:(NSNotification *)notification
{
NSString *name = notification.name;
if ([name isEqualToString:UIKeyboardWillShowNotification]) {
return SLKKeyboardWillShowNotification;
}
if ([name isEqualToString:UIKeyboardWillHideNotification]) {
return SLKKeyboardWillHideNotification;
}
if ([name isEqualToString:UIKeyboardDidShowNotification]) {
return SLKKeyboardDidShowNotification;
}
if ([name isEqualToString:UIKeyboardDidHideNotification]) {
return SLKKeyboardDidHideNotification;
}
return nil;
}
- (SLKKeyboardStatus)slk_keyboardStatusForNotification:(NSNotification *)notification
{
NSString *name = notification.name;
if ([name isEqualToString:UIKeyboardWillShowNotification]) {
return SLKKeyboardStatusWillShow;
}
if ([name isEqualToString:UIKeyboardDidShowNotification]) {
return SLKKeyboardStatusDidShow;
}
if ([name isEqualToString:UIKeyboardWillHideNotification]) {
return SLKKeyboardStatusWillHide;
}
if ([name isEqualToString:UIKeyboardDidHideNotification]) {
return SLKKeyboardStatusDidHide;
}
return -1;
}
- (BOOL)slk_isIllogicalKeyboardStatus:(SLKKeyboardStatus)newStatus
{
if ((self.keyboardStatus == SLKKeyboardStatusDidHide && newStatus == SLKKeyboardStatusWillShow) ||
(self.keyboardStatus == SLKKeyboardStatusWillShow && newStatus == SLKKeyboardStatusDidShow) ||
(self.keyboardStatus == SLKKeyboardStatusDidShow && newStatus == SLKKeyboardStatusWillHide) ||
(self.keyboardStatus == SLKKeyboardStatusWillHide && newStatus == SLKKeyboardStatusDidHide)) {
return NO;
}
return YES;
}
#pragma mark - Setters
- (void)setEdgesForExtendedLayout:(UIRectEdge)rectEdge
{
if (self.edgesForExtendedLayout == rectEdge) {
return;
}
[super setEdgesForExtendedLayout:rectEdge];
[self slk_updateViewConstraints];
}
- (void)setScrollViewProxy:(UIScrollView *)scrollView
{
if ([_scrollViewProxy isEqual:scrollView]) {
return;
}
_singleTapGesture = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(slk_didTapScrollView:)];
_singleTapGesture.delegate = self;
[_singleTapGesture requireGestureRecognizerToFail:scrollView.panGestureRecognizer];
[scrollView addGestureRecognizer:self.singleTapGesture];
[scrollView.panGestureRecognizer addTarget:self action:@selector(slk_didPanTextInputBar:)];
_scrollViewProxy = scrollView;
}
- (void)setAutoCompleting:(BOOL)autoCompleting
{
if (_autoCompleting == autoCompleting) {
return;
}
_autoCompleting = autoCompleting;
self.scrollViewProxy.scrollEnabled = !autoCompleting;
}
- (void)setBarState:(SLKInputBarState)barState
{
_barState = barState;
_textInputbar.barState = barState;
}
- (void)setInverted:(BOOL)inverted
{
if (_inverted == inverted) {
return;
}
_inverted = inverted;
self.scrollViewProxy.transform = inverted ? CGAffineTransformMake(1, 0, 0, -1, 0, 0) : CGAffineTransformIdentity;
}
- (void)setBounces:(BOOL)bounces
{
_bounces = bounces;
_textInputbar.bounces = bounces;
}
- (BOOL)slk_updateKeyboardStatus:(SLKKeyboardStatus)status
{
// Skips if trying to update the same status
if (_keyboardStatus == status) {
return NO;
}
// Skips illogical conditions
// Forces the keyboard status when didHide to avoid any inconsistency.
if (status != SLKKeyboardStatusDidHide && [self slk_isIllogicalKeyboardStatus:status]) {
return NO;
}
_keyboardStatus = status;
[self didChangeKeyboardStatus:status];
return YES;
}
#pragma mark - Public & Subclassable Methods
- (void)setMenuAccesoryView:(UIView *)menuAccesoryView
{
//remove previous view from super view if exist
[_menuAccesoryView removeFromSuperview];
//add view
[self.view addSubview:menuAccesoryView];
_menuAccesoryView = menuAccesoryView;
}
- (void)presentMenuAccessoryView:(BOOL)animated
{
if(_menuAccesoryView == nil ||
_menuAccesoryView.superview == nil) {
return;
}
[UIView setAnimationsEnabled:NO];
[self dismissKeyboard:NO];
[self.textView setUserInteractionEnabled:NO];
[self slk_dismissTextInputbarIfNeeded];
self.barState = _textInputbar.barState;
[UIView setAnimationsEnabled:YES];
}
- (void)dismissMenuAccessoryView:(BOOL)animated
{
if(_menuAccesoryView == nil ||
_menuAccesoryView.superview == nil) {
return;
}
[UIView setAnimationsEnabled:NO];
[self.menuAccesoryView removeFromSuperview];
self.menuAccesoryView = nil;
_textInputbar.barState = self.barState;
[_textInputbar.rightButton setEnabled:![self.textView.text isEqualToString:@""]];
if(self.keyboardStatus != SLKKeyboardStatusDidShow &&
self.keyboardStatus != SLKKeyboardStatusWillShow) {
self.keyboardHC.constant = [self slk_appropriateBottomMargin];
}
[self slk_refreshViewContraints];
[UIView setAnimationsEnabled:YES];
}
- (void)presentKeyboard:(BOOL)animated
{
[self.leftButton setSelected:NO];
[self.textView setUserInteractionEnabled:YES];
// Skips if already first responder
if ([self.textView isFirstResponder]) {
return;
}
if (!animated) {
[UIView performWithoutAnimation:^{
[self.textView becomeFirstResponder];
}];
}
else {
[self.textView becomeFirstResponder];
}
}
- (void)dismissKeyboard:(BOOL)animated
{
[self.textView setUserInteractionEnabled:NO];
// Dismisses the keyboard from any first responder in the window.
if (![self.textView isFirstResponder] && self.keyboardHC.constant > 0) {
[self.view.window endEditing:NO];
}
if (!animated) {
[UIView performWithoutAnimation:^{
[self.textView resignFirstResponder];
}];
}
else {
[self.textView resignFirstResponder];
}
}
- (BOOL)forceTextInputbarAdjustmentForResponder:(UIResponder *)responder
{
return NO;
}
- (BOOL)ignoreTextInputbarAdjustment
{
if (self.isExternalKeyboardDetected || self.isKeyboardUndocked) {
return YES;
}
return NO;
}
- (void)didChangeKeyboardStatus:(SLKKeyboardStatus)status
{
// No implementation here. Meant to be overriden in subclass.
}
- (void)textWillUpdate
{
// No implementation here. Meant to be overriden in subclass.
}
- (void)textDidUpdate:(BOOL)animated
{
if (self.isTextInputbarHidden) {
return;
}
CGFloat inputbarHeight = _textInputbar.appropriateHeight;
_textInputbar.rightButton.enabled = [self canPressRightButton];
_textInputbar.editorRightButton.enabled = [self canPressRightButton];
if (inputbarHeight != self.textInputbarHC.constant)
{
CGFloat inputBarHeightDelta = inputbarHeight - self.textInputbarHC.constant;
self.textInputbarHC.constant = inputbarHeight;
self.scrollViewHC.constant = [self slk_appropriateScrollViewHeight];
[self.view layoutIfNeeded];
}
// Toggles auto-correction if requiered
[self slk_enableTypingSuggestionIfNeeded];
}
- (void)textSelectionDidChange
{
// The text view must be first responder
if (![self.textView isFirstResponder] || self.keyboardStatus != SLKKeyboardStatusDidShow) {
return;
}
// Skips there is a real text selection
if (self.textView.isTrackpadEnabled) {
return;
}
if (self.textView.selectedRange.length > 0) {
if (self.isAutoCompleting && [self shouldProcessTextForAutoCompletion]) {
[self cancelAutoCompletion];
}
return;
}
// Process the text at every caret movement
[self slk_processTextForAutoCompletion];
}
- (BOOL)canPressRightButton
{
NSString *text = [self.textView.text stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]];
if (text.length > 0 && ![_textInputbar limitExceeded]) {
return YES;
}
return self.alwaysEnableRightButton;
}
- (void)didPressLeftButton:(id)sender
{
// No implementation here. Meant to be overriden in subclass.
}
- (void)didPressRightButton:(id)sender
{
if (self.shouldClearTextAtRightButtonPress) {
// Clears the text and the undo manager
[self.textView slk_clearText:YES];
}
// reset left button
[self.leftButton setSelected:NO];
// Clears cache
[self clearCachedText];
}
- (void)editText:(NSString *)text
{
NSAttributedString *attributedText = [self.textView slk_defaultAttributedStringForText:text];
[self editAttributedText:attributedText];
}
- (void)editAttributedText:(NSAttributedString *)attributedText
{
if (![_textInputbar canEditText:attributedText.string]) {
return;
}
// Caches the current text, in case the user cancels the edition
[self slk_cacheAttributedTextToDisk:self.textView.attributedText];
[_textInputbar beginTextEditing];
// Setting the text after calling -beginTextEditing is safer
[self.textView setAttributedText:attributedText];
[self.textView slk_scrollToCaretPositonAnimated:YES];
// Brings up the keyboard if needed
[self presentKeyboard:YES];
}
- (void)didCommitTextEditing:(id)sender
{
if (!_textInputbar.isEditing) {
return;
}
[_textInputbar endTextEdition];
// Clears the text and but not the undo manager
[self.textView slk_clearText:NO];
}
- (void)didCancelTextEditing:(id)sender
{
if (!_textInputbar.isEditing) {
return;
}
[_textInputbar endTextEdition];
// Clears the text and but not the undo manager
[self.textView slk_clearText:NO];
// Restores any previous cached text before entering in editing mode
[self slk_reloadTextView];
}
- (BOOL)canShowTypingIndicator
{
// Don't show if the text is being edited or auto-completed.
if (_textInputbar.isEditing || self.isAutoCompleting) {
return NO;
}
return YES;
}
- (CGFloat)heightForAutoCompletionView
{
return 0.0;
}
- (CGFloat)maximumHeightForAutoCompletionView
{
CGFloat maxiumumHeight = SLKAutoCompletionViewDefaultHeight;
if (self.isAutoCompleting) {
CGFloat scrollViewHeight = self.scrollViewHC.constant;
scrollViewHeight -= [self slk_topBarsHeight];
if (scrollViewHeight < maxiumumHeight) {
maxiumumHeight = scrollViewHeight;
}
}
return maxiumumHeight;
}
- (void)didPasteMediaContent:(NSDictionary *)userInfo
{
// No implementation here. Meant to be overriden in subclass.
}
- (void)willRequestUndo
{
NSString *title = NSLocalizedString(@"Undo Typing", nil);
NSString *acceptTitle = NSLocalizedString(@"Undo", nil);
NSString *cancelTitle = NSLocalizedString(@"Cancel", nil);
#ifdef __IPHONE_8_0
UIAlertController *alertController = [UIAlertController alertControllerWithTitle:title message:nil preferredStyle:UIAlertControllerStyleAlert];
[alertController addAction:[UIAlertAction actionWithTitle:acceptTitle style:UIAlertActionStyleDefault handler:^(UIAlertAction *action) {
// Clears the text but doesn't clear the undo manager
if (self.shakeToClearEnabled) {
[self.textView slk_clearText:NO];
}
}]];
[alertController addAction:[UIAlertAction actionWithTitle:cancelTitle style:UIAlertActionStyleCancel handler:NULL]];
[self presentViewController:alertController animated:YES completion:nil];
#else
UIAlertView *alert = [UIAlertView new];
[alert setTitle:title];
[alert addButtonWithTitle:acceptTitle];
[alert addButtonWithTitle:cancelTitle];
[alert setCancelButtonIndex:1];
[alert setTag:kSLKAlertViewClearTextTag];
[alert setDelegate:self];
[alert show];
#endif
}
- (void)setTextInputbarHidden:(BOOL)hidden
{
[self setTextInputbarHidden:hidden animated:NO];
}
- (void)setTextInputbarHidden:(BOOL)hidden animated:(BOOL)animated
{
if (self.isTextInputbarHidden == hidden) {
return;
}
_textInputbar.hidden = hidden;
__weak typeof(self) weakSelf = self;
void (^animations)() = ^void(){
weakSelf.textInputbarHC.constant = hidden ? 0.0 : weakSelf.textInputbar.appropriateHeight;
[weakSelf.view layoutIfNeeded];
if (@available(iOS 11.0, *)) {
[self slk_refreshViewContraints];
}
};
void (^completion)(BOOL finished) = ^void(BOOL finished){
if (hidden) {
[self dismissKeyboard:YES];
}
};
if (animated) {
[UIView animateWithDuration:0.25 animations:animations completion:completion];
}
else {
animations();
completion(NO);
}
}
#pragma mark - Private Methods
- (void)slk_didPanTextInputBar:(UIPanGestureRecognizer *)gesture
{
// Textinput dragging isn't supported when
if (!self.view.window || !self.keyboardPanningEnabled ||
[self ignoreTextInputbarAdjustment] || self.isPresentedInPopover) {
return;
}
dispatch_async(dispatch_get_main_queue(), ^{
[self slk_handlePanGestureRecognizer:gesture];
});
}
- (void)slk_handlePanGestureRecognizer:(UIPanGestureRecognizer *)gesture
{
// Local variables
static CGPoint startPoint;
static CGRect originalFrame;
static BOOL dragging = NO;
static BOOL presenting = NO;
__block UIView *keyboardView = [_textInputbar.inputAccessoryView keyboardViewProxy];
// When no keyboard view has been detecting, let's skip any handling.
if (!keyboardView) {
return;
}
// Dynamic variables
CGPoint gestureLocation = [gesture locationInView:self.view];
CGPoint gestureVelocity = [gesture velocityInView:self.view];
CGFloat keyboardMaxY = CGRectGetHeight(SLKKeyWindowBounds());
CGFloat keyboardMinY = keyboardMaxY - CGRectGetHeight(keyboardView.frame);
// Skips this if it's not the expected textView.
// Checking the keyboard height constant helps to disable the view constraints update on iPad when the keyboard is undocked.
// Checking the keyboard status allows to keep the inputAccessoryView valid when still reacing the bottom of the screen.
CGFloat bottomMargin = [self slk_appropriateBottomMargin];
if (![self.textView isFirstResponder] || (self.keyboardHC.constant == bottomMargin && self.keyboardStatus == SLKKeyboardStatusDidHide)) {
#if SLKBottomPanningEnabled
if ([gesture.view isEqual:self.scrollViewProxy]) {
if (gestureVelocity.y > 0) {
return;
}
else if ((self.isInverted && ![self.scrollViewProxy slk_isAtTop]) || (!self.isInverted && ![self.scrollViewProxy slk_isAtBottom])) {
return;
}
}
presenting = YES;
#else
if ([gesture.view isEqual:_textInputbar] && gestureVelocity.y < 0) {
[self presentKeyboard:YES];
}
return;
#endif
}
switch (gesture.state) {
case UIGestureRecognizerStateBegan: {
startPoint = CGPointZero;
dragging = NO;
if (presenting) {
// Let's first present the keyboard without animation
[self presentKeyboard:NO];
// So we can capture the keyboard's view
keyboardView = [_textInputbar.inputAccessoryView keyboardViewProxy];
originalFrame = keyboardView.frame;
originalFrame.origin.y = CGRectGetMaxY(self.view.frame);
// And move the keyboard to the bottom edge
// TODO: Fix an occasional layout glitch when the keyboard appears for the first time.
keyboardView.frame = originalFrame;
}
break;
}
case UIGestureRecognizerStateChanged: {
if (CGRectContainsPoint(_textInputbar.frame, gestureLocation) || dragging || presenting){
if (CGPointEqualToPoint(startPoint, CGPointZero)) {
startPoint = gestureLocation;
dragging = YES;
if (!presenting) {
originalFrame = keyboardView.frame;
}
}
self.movingKeyboard = YES;
CGPoint transition = CGPointMake(gestureLocation.x - startPoint.x, gestureLocation.y - startPoint.y);
CGRect keyboardFrame = originalFrame;
if (presenting) {
keyboardFrame.origin.y += transition.y;
}
else {
keyboardFrame.origin.y += MAX(transition.y, 0.0);
}
// Makes sure they keyboard is always anchored to the bottom
if (CGRectGetMinY(keyboardFrame) < keyboardMinY) {
keyboardFrame.origin.y = keyboardMinY;
}
keyboardView.frame = keyboardFrame;
self.keyboardHC.constant = [self slk_appropriateKeyboardHeightFromRect:keyboardFrame];
self.scrollViewHC.constant = [self slk_appropriateScrollViewHeight];
// layoutIfNeeded must be called before any further scrollView internal adjustments (content offset and size)
[self.view layoutIfNeeded];
// Overrides the scrollView's contentOffset to allow following the same position when dragging the keyboard
CGPoint offset = _scrollViewOffsetBeforeDragging;
if (self.isInverted) {
if (!self.scrollViewProxy.isDecelerating && self.scrollViewProxy.isTracking) {
self.scrollViewProxy.contentOffset = _scrollViewOffsetBeforeDragging;
}
}
else {
CGFloat keyboardHeightDelta = _keyboardHeightBeforeDragging-self.keyboardHC.constant;
offset.y -= keyboardHeightDelta;
self.scrollViewProxy.contentOffset = offset;
}
}
break;
}
case UIGestureRecognizerStatePossible:
case UIGestureRecognizerStateCancelled:
case UIGestureRecognizerStateEnded:
case UIGestureRecognizerStateFailed: {
if (!dragging) {
break;
}
CGPoint transition = CGPointMake(0.0, fabs(gestureLocation.y - startPoint.y));
CGRect keyboardFrame = originalFrame;
if (presenting) {
keyboardFrame.origin.y = keyboardMinY;
}
// The velocity can be changed to hide or show the keyboard based on the gesture
CGFloat minVelocity = 20.0;
CGFloat minDistance = CGRectGetHeight(keyboardFrame)/2.0;
BOOL hide = (gestureVelocity.y > minVelocity) || (presenting && transition.y < minDistance) || (!presenting && transition.y > minDistance);
if (hide) keyboardFrame.origin.y = keyboardMaxY;
self.keyboardHC.constant = [self slk_appropriateKeyboardHeightFromRect:keyboardFrame];
self.scrollViewHC.constant = [self slk_appropriateScrollViewHeight];
[UIView animateWithDuration:0.25
delay:0.0
options:UIViewAnimationOptionCurveEaseInOut|UIViewAnimationOptionBeginFromCurrentState
animations:^{
[self.view layoutIfNeeded];
keyboardView.frame = keyboardFrame;
}
completion:^(BOOL finished) {
if (hide) {
[self dismissKeyboard:NO];
}
// Tear down
startPoint = CGPointZero;
originalFrame = CGRectZero;
dragging = NO;
presenting = NO;
self.movingKeyboard = NO;
}];
break;
}
default:
break;
}
}
- (void)slk_didTapScrollView:(UIGestureRecognizer *)gesture
{
if (!self.isPresentedInPopover && ![self ignoreTextInputbarAdjustment]) {
[self dismissKeyboard:YES];
}
}
- (void)slk_didPanTextView:(UIGestureRecognizer *)gesture
{
[self presentKeyboard:YES];
}
- (void)slk_performRightAction
{
NSArray *actions = [self.rightButton actionsForTarget:self forControlEvent:UIControlEventTouchUpInside];
if (actions.count > 0 && [self canPressRightButton]) {
[self.rightButton sendActionsForControlEvents:UIControlEventTouchUpInside];
}
}
- (void)slk_postKeyboarStatusNotification:(NSNotification *)notification
{
if ([self ignoreTextInputbarAdjustment] || self.isTransitioning) {
return;
}
NSMutableDictionary *userInfo = [notification.userInfo mutableCopy];
CGRect beginFrame = [notification.userInfo[UIKeyboardFrameBeginUserInfoKey] CGRectValue];
CGRect endFrame = [notification.userInfo[UIKeyboardFrameEndUserInfoKey] CGRectValue];
// Fixes iOS7 oddness with inverted values on landscape orientation
if (!SLK_IS_IOS8_AND_HIGHER && SLK_IS_LANDSCAPE) {
beginFrame = SLKRectInvert(beginFrame);
endFrame = SLKRectInvert(endFrame);
}
CGFloat keyboardHeight = CGRectGetHeight(endFrame);
beginFrame.size.height = keyboardHeight;
endFrame.size.height = keyboardHeight;
[userInfo setObject:[NSValue valueWithCGRect:beginFrame] forKey:UIKeyboardFrameBeginUserInfoKey];
[userInfo setObject:[NSValue valueWithCGRect:endFrame] forKey:UIKeyboardFrameEndUserInfoKey];
NSString *name = [self slk_appropriateKeyboardNotificationName:notification];
[[NSNotificationCenter defaultCenter] postNotificationName:name object:self.textView userInfo:userInfo];
}
- (void)slk_enableTypingSuggestionIfNeeded
{
if (![self.textView isFirstResponder]) {
return;
}
BOOL enable = !self.isAutoCompleting;
NSString *inputPrimaryLanguage = self.textView.textInputMode.primaryLanguage;
// Toggling autocorrect on Japanese keyboards breaks autocompletion by replacing the autocompletion prefix by an empty string.
// So for now, let's not disable autocorrection for Japanese.
if ([inputPrimaryLanguage isEqualToString:@"ja-JP"]) {
return;
}
// Let's avoid refreshing the text view while dictation mode is enabled.
// This solves a crash some users were experiencing when auto-completing with the dictation input mode.
if ([inputPrimaryLanguage isEqualToString:@"dictation"]) {
return;
}
if (enable == NO && ![self shouldDisableTypingSuggestionForAutoCompletion]) {
return;
}
[self.textView setTypingSuggestionEnabled:enable];
}
- (void)slk_dismissTextInputbarIfNeeded
{
CGFloat bottomMargin = [self slk_appropriateBottomMargin];
if (self.keyboardHC.constant == bottomMargin) {
return;
}
self.keyboardHC.constant = bottomMargin;
self.scrollViewHC.constant = [self slk_appropriateScrollViewHeight];
[self slk_hideAutoCompletionViewIfNeeded];
[self.view layoutIfNeeded];
}
- (void)slk_detectKeyboardStatesInNotification:(NSNotification *)notification
{
// Tear down
_externalKeyboardDetected = NO;
_keyboardUndocked = NO;
if (self.isMovingKeyboard) {
return;
}
// Based on http://stackoverflow.com/a/5760910/287403
// 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.
// If it's greater(or equal) the window height, it's an external keyboard.
CGRect beginRect = [notification.userInfo[UIKeyboardFrameBeginUserInfoKey] CGRectValue];
CGRect endRect = [notification.userInfo[UIKeyboardFrameEndUserInfoKey] CGRectValue];
// Grab the base view for conversions as we don't want window coordinates in < iOS 8
// iOS 8 fixes the whole coordinate system issue for us, but iOS 7 doesn't rotate the app window coordinate space.
UIView *baseView = self.view.window.rootViewController.view;
CGRect screenBounds = [UIScreen mainScreen].bounds;
// Convert the main screen bounds into the correct coordinate space but ignore the origin.
CGRect viewBounds = [self.view convertRect:SLKKeyWindowBounds() fromView:nil];
viewBounds = CGRectMake(0, 0, viewBounds.size.width, viewBounds.size.height);
// We want these rects in the correct coordinate space as well.
CGRect convertBegin = [baseView convertRect:beginRect fromView:nil];
CGRect convertEnd = [baseView convertRect:endRect fromView:nil];
if ([notification.name isEqualToString:UIKeyboardWillShowNotification]) {
if (convertEnd.origin.y >= viewBounds.size.height) {
_externalKeyboardDetected = YES;
}
}
else if ([notification.name isEqualToString:UIKeyboardWillHideNotification]) {
// The additional logic check here (== to width) accounts for a glitch (iOS 8 only?) where the window has rotated it's coordinates
// but the beginRect doesn't yet reflect that. It should never cause a false positive.
if (convertBegin.origin.y >= viewBounds.size.height ||
convertBegin.origin.y == viewBounds.size.width) {
_externalKeyboardDetected = YES;
}
}
if (SLK_IS_IPAD && CGRectGetMaxY(convertEnd) < CGRectGetMaxY(screenBounds)) {
// The keyboard is undocked or split (iPad Only)
_keyboardUndocked = YES;
// An external keyboard cannot be detected anymore
_externalKeyboardDetected = NO;
}
}
- (void)slk_adjustContentConfigurationIfNeeded
{
UIEdgeInsets contentInset = self.scrollViewProxy.contentInset;
// When inverted, we need to substract the top bars height (generally status bar + navigation bar's) to align the top of the
// scrollView correctly to its top edge.
if (self.inverted) {
contentInset.bottom = [self slk_topBarsHeight];
contentInset.top = contentInset.bottom > 0.0 ? 0.0 : contentInset.top;
}
else {
contentInset.bottom = 0.0;
}
self.scrollViewProxy.contentInset = contentInset;
self.scrollViewProxy.scrollIndicatorInsets = contentInset;
}
- (void)slk_prepareForInterfaceTransitionWithDuration:(NSTimeInterval)duration
{
self.transitioning = YES;
[self.view layoutIfNeeded];
if ([self.textView isFirstResponder]) {
[self.textView slk_scrollToCaretPositonAnimated:NO];
}
else {
[self.textView slk_scrollToBottomAnimated:NO];
}
// Disables the flag after the rotation animation is finished
// Hacky but works.
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(duration * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
self.transitioning = NO;
});
}
#pragma mark - Keyboard Events
- (void)didPressReturnKey:(UIKeyCommand *)keyCommand
{
if (_textInputbar.isEditing) {
[self didCommitTextEditing:keyCommand];
}
else {
[self slk_performRightAction];
}
}
- (void)didPressEscapeKey:(UIKeyCommand *)keyCommand
{
if (self.isAutoCompleting) {
[self cancelAutoCompletion];
}
else if (_textInputbar.isEditing) {
[self didCancelTextEditing:keyCommand];
}
CGFloat bottomMargin = [self slk_appropriateBottomMargin];
if ([self ignoreTextInputbarAdjustment] || ([self.textView isFirstResponder] && self.keyboardHC.constant == bottomMargin)) {
return;
}
[self dismissKeyboard:YES];
}
- (void)didPressArrowKey:(UIKeyCommand *)keyCommand
{
[self.textView didPressArrowKey:keyCommand];
}
#pragma mark - Notification Events
- (void)slk_willShowOrHideKeyboard:(NSNotification *)notification
{
SLKKeyboardStatus status = [self slk_keyboardStatusForNotification:notification];
// Skips if the view isn't visible.
if (!self.isViewVisible) {
return;
}
// Skips if it is presented inside of a popover.
if (self.isPresentedInPopover) {
return;
}
// Skips if textview did refresh only.
if (self.textView.didNotResignFirstResponder) {
return;
}
UIResponder *currentResponder = [UIResponder slk_currentFirstResponder];
// Skips if it's not the expected textView and shouldn't force adjustment of the text input bar.
// This will also dismiss the text input bar if it's visible, and exit auto-completion mode if enabled.
if (currentResponder && ![currentResponder isEqual:self.textView] && ![self forceTextInputbarAdjustmentForResponder:currentResponder]) {
[self slk_dismissTextInputbarIfNeeded];
return;
}
// Skips if it's the current status
if (self.keyboardStatus == status) {
return;
}
// BOOL shouldUpdateOffset = self.keyboardHC.constant == self.textInputBarBC &&
// self.menuAccesoryView == nil;
// if (status == SLKKeyboardStatusWillShow && shouldUpdateOffset) {
// CGFloat offset = SLK_IS_IPHONE6PLUS ? 231 : 221;
// [self.tableView setContentOffset:CGPointMake(0, self.tableView.contentOffset.y + offset)];
// }
// Programatically stops scrolling before updating the view constraints (to avoid scrolling glitch).
if (status == SLKKeyboardStatusWillShow) {
[self.scrollViewProxy slk_stopScrolling];
}
// Stores the previous keyboard height
CGFloat previousKeyboardHeight = self.keyboardHC.constant;
// Updates the height constraints' constants
self.keyboardHC.constant = [self slk_appropriateKeyboardHeightFromNotification:notification];
self.scrollViewHC.constant = [self slk_appropriateScrollViewHeight];
// Updates and notifies about the keyboard status update
if ([self slk_updateKeyboardStatus:status]) {
// Posts custom keyboard notification, if logical conditions apply
[self slk_postKeyboarStatusNotification:notification];
}
// Hides the auto-completion view if the keyboard is being dismissed.
if (![self.textView isFirstResponder] || status == SLKKeyboardStatusWillHide) {
[self slk_hideAutoCompletionViewIfNeeded];
}
UIScrollView *scrollView = self.scrollViewProxy;
NSInteger curve = [notification.userInfo[UIKeyboardAnimationCurveUserInfoKey] integerValue];
NSTimeInterval duration = [notification.userInfo[UIKeyboardAnimationDurationUserInfoKey] doubleValue];
CGRect beginFrame = [notification.userInfo[UIKeyboardFrameBeginUserInfoKey] CGRectValue];
CGRect endFrame = [notification.userInfo[UIKeyboardFrameEndUserInfoKey] CGRectValue];
void (^animations)() = ^void() {
// Scrolls to bottom only if the keyboard is about to show.
if (self.shouldScrollToBottomAfterKeyboardShows &&
self.keyboardStatus == SLKKeyboardStatusWillShow &&
[self.textView isFirstResponder]) { //scrollDown only if first responder is textView
if (self.isInverted) {
[scrollView slk_scrollToTopAnimated:YES];
}
else {
[scrollView slk_scrollToBottomAnimated:YES];
}
}
};
// Begin and end frames are the same when the keyboard is shown during navigation controller's push animation.
// The animation happens in window coordinates (slides from right to left) but doesn't in the view controller's view coordinates.
// Second condition: check if the height of the keyboard changed.
if (!CGRectEqualToRect(beginFrame, endFrame) || fabs(previousKeyboardHeight - self.keyboardHC.constant) > 0.0)
{
// Content Offset correction if not inverted and not auto-completing.
if (!self.isInverted && !self.isAutoCompleting) {
CGFloat scrollViewHeight = self.scrollViewHC.constant;
CGFloat keyboardHeight = self.keyboardHC.constant;
CGSize contentSize = scrollView.contentSize;
CGPoint contentOffset = scrollView.contentOffset;
CGFloat newOffset = MIN(contentSize.height - scrollViewHeight,
contentOffset.y + keyboardHeight - previousKeyboardHeight);
scrollView.contentOffset = CGPointMake(contentOffset.x, newOffset);
}
// 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.
[self.view slk_animateLayoutIfNeededWithDuration:duration
bounce:NO
options:(curve<<16)|UIViewAnimationOptionLayoutSubviews|UIViewAnimationOptionBeginFromCurrentState
animations:animations
completion:NULL];
}
else {
animations();
}
}
- (void)slk_didShowOrHideKeyboard:(NSNotification *)notification
{
SLKKeyboardStatus status = [self slk_keyboardStatusForNotification:notification];
// Skips if the view isn't visible
if (!self.isViewVisible) {
if (status == SLKKeyboardStatusDidHide && self.keyboardStatus == SLKKeyboardStatusWillHide) {
// Even if the view isn't visible anymore, let's still continue to update all states.
}
else {
return;
}
}
// Skips if it is presented inside of a popover
if (self.isPresentedInPopover) {
return;
}
// Skips if textview did refresh only
if (self.textView.didNotResignFirstResponder) {
return;
}
// Skips if it's the current status
if (self.keyboardStatus == status) {
return;
}
// Updates and notifies about the keyboard status update
if ([self slk_updateKeyboardStatus:status]) {
// Posts custom keyboard notification, if logical conditions apply
[self slk_postKeyboarStatusNotification:notification];
}
// After showing keyboard, check if the current cursor position could diplay autocompletion
if ([self.textView isFirstResponder] && status == SLKKeyboardStatusDidShow && !self.isAutoCompleting) {
// Wait till the end of the current run loop
dispatch_async(dispatch_get_main_queue(), ^{
[self slk_processTextForAutoCompletion];
});
}
// Very important to invalidate this flag after the keyboard is dismissed or presented, to start with a clean state next time.
self.movingKeyboard = NO;
}
- (void)slk_didPostSLKKeyboardNotification:(NSNotification *)notification
{
if (![notification.object isEqual:self.textView]) {
return;
}
// Used for debug only
NSLog(@"%@ %s: %@", NSStringFromClass([self class]), __FUNCTION__, notification);
}
- (void)slk_willChangeTextViewText:(NSNotification *)notification
{
// Skips this it's not the expected textView.
if (![notification.object isEqual:self.textView]) {
return;
}
[self textWillUpdate];
}
- (void)slk_didChangeTextViewText:(NSNotification *)notification
{
// Skips this it's not the expected textView.
if (![notification.object isEqual:self.textView]) {
return;
}
// Animated only if the view already appeared.
[self textDidUpdate:self.isViewVisible];
// Process the text at every change, when the view is visible
if (self.isViewVisible) {
[self slk_processTextForAutoCompletion];
}
}
- (void)slk_didChangeTextViewContentSize:(NSNotification *)notification
{
// Skips this it's not the expected textView.
if (![notification.object isEqual:self.textView]) {
return;
}
// Animated only if the view already appeared.
[self textDidUpdate:self.isViewVisible];
}
- (void)slk_didChangeTextViewSelectedRange:(NSNotification *)notification
{
// Skips this it's not the expected textView.
if (![notification.object isEqual:self.textView]) {
return;
}
[self textSelectionDidChange];
}
- (void)slk_didChangeTextViewPasteboard:(NSNotification *)notification
{
// Skips this if it's not the expected textView.
if (![self.textView isFirstResponder]) {
return;
}
// Notifies only if the pasted item is nested in a dictionary.
if (notification.userInfo) {
[self didPasteMediaContent:notification.userInfo];
}
}
- (void)slk_didShakeTextView:(NSNotification *)notification
{
// Skips this if it's not the expected textView.
if (![self.textView isFirstResponder]) {
return;
}
// Notifies of the shake gesture if undo mode is on and the text view is not empty
if (self.shakeToClearEnabled && self.textView.text.length > 0) {
[self willRequestUndo];
}
}
- (void)slk_willShowOrHideTypeIndicatorView:(UIView <SLKTypingIndicatorProtocol> *)view
{
// Skips if the typing indicator should not show. Ignores the checking if it's trying to hide.
if (![self canShowTypingIndicator] && view.isVisible) {
return;
}
CGFloat systemLayoutSizeHeight = [view systemLayoutSizeFittingSize:UILayoutFittingCompressedSize].height;
CGFloat height = view.isVisible ? systemLayoutSizeHeight : 0.0;
self.typingIndicatorViewHC.constant = height;
self.scrollViewHC.constant -= height;
if (view.isVisible) {
view.hidden = NO;
}
[self.view slk_animateLayoutIfNeededWithBounce:self.bounces
options:UIViewAnimationOptionCurveEaseInOut
animations:NULL
completion:^(BOOL finished) {
if (!view.isVisible) {
view.hidden = YES;
}
}];
}
#pragma mark - KVO Events
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
{
if ([object conformsToProtocol:@protocol(SLKTypingIndicatorProtocol)] && [keyPath isEqualToString:@"visible"]) {
[self slk_willShowOrHideTypeIndicatorView:object];
}
else {
[super observeValueForKeyPath:keyPath ofObject:object change:change context:context];
}
}
#pragma mark - Auto-Completion Text Processing
- (void)registerPrefixesForAutoCompletion:(NSArray <NSString *> *)prefixes
{
if (prefixes.count == 0) {
return;
}
NSMutableSet *set = [NSMutableSet setWithSet:self.registeredPrefixes];
[set addObjectsFromArray:[prefixes copy]];
_registeredPrefixes = [NSSet setWithSet:set];
}
- (BOOL)shouldProcessTextForAutoCompletion:(NSString *)text
{
return [self shouldProcessTextForAutoCompletion];
}
- (BOOL)shouldProcessTextForAutoCompletion
{
if (!_registeredPrefixes || _registeredPrefixes.count == 0) {
return NO;
}
return YES;
}
- (BOOL)shouldDisableTypingSuggestionForAutoCompletion
{
if (!_registeredPrefixes || _registeredPrefixes.count == 0) {
return NO;
}
return YES;
}
- (void)didChangeAutoCompletionPrefix:(NSString *)prefix andWord:(NSString *)word
{
// No implementation here. Meant to be overriden in subclass.
}
- (void)showAutoCompletionView:(BOOL)show
{
// Reloads the tableview before showing/hiding
if (show) {
[_autoCompletionView reloadData];
}
self.autoCompleting = show;
// Toggles auto-correction if requiered
[self slk_enableTypingSuggestionIfNeeded];
CGFloat viewHeight = show ? [self heightForAutoCompletionView] : 0.0;
[self.autoCompletionBackgroundView setHidden:!show];
if (self.autoCompletionViewHC.constant == viewHeight) {
return;
}
// If the auto-completion view height is bigger than the maximum height allows, it is reduce to that size. Default 140 pts.
CGFloat maximumHeight = [self maximumHeightForAutoCompletionView];
if (viewHeight > maximumHeight) {
viewHeight = maximumHeight;
}
CGFloat contentViewHeight = self.scrollViewHC.constant + self.autoCompletionViewHC.constant;
// On iPhone, the auto-completion view can't extend beyond the content view height
if (SLK_IS_IPHONE && viewHeight > contentViewHeight) {
viewHeight = contentViewHeight;
}
self.autoCompletionViewHC.constant = viewHeight;
[self.view slk_animateLayoutIfNeededWithBounce:self.bounces
options:UIViewAnimationOptionCurveEaseInOut|UIViewAnimationOptionLayoutSubviews|UIViewAnimationOptionBeginFromCurrentState|UIViewAnimationOptionAllowUserInteraction
animations:NULL];
}
- (void)showAutoCompletionViewWithPrefix:(NSString *)prefix andWord:(NSString *)word prefixRange:(NSRange)prefixRange
{
if ([self.registeredPrefixes containsObject:prefix]) {
_foundPrefix = prefix;
_foundWord = word;
_foundPrefixRange = prefixRange;
[self didChangeAutoCompletionPrefix:self.foundPrefix andWord:self.foundWord];
[self showAutoCompletionView:YES];
}
}
- (void)acceptAutoCompletionWithString:(NSString *)string
{
[self acceptAutoCompletionWithString:string keepPrefix:YES];
}
- (void)acceptAutoCompletionWithString:(NSString *)string keepPrefix:(BOOL)keepPrefix
{
if (string.length == 0) {
return;
}
NSUInteger location = self.foundPrefixRange.location;
if (keepPrefix) {
location += self.foundPrefixRange.length;
}
NSUInteger length = self.foundWord.length;
if (!keepPrefix) {
length += self.foundPrefixRange.length;
}
NSRange range = NSMakeRange(location, length);
NSRange insertionRange = [self.textView slk_insertText:string inRange:range];
self.textView.selectedRange = NSMakeRange(insertionRange.location, 0);
[self.textView slk_scrollToCaretPositonAnimated:YES];
[self cancelAutoCompletion];
}
- (void)cancelAutoCompletion
{
[self slk_invalidateAutoCompletion];
[self slk_hideAutoCompletionViewIfNeeded];
}
- (void)slk_processTextForAutoCompletion
{
NSString *text = self.textView.text;
if ((!self.isAutoCompleting && text.length == 0) ||
self.isTransitioning ||
![self shouldProcessTextForAutoCompletion]) {
return;
}
[self.textView lookForPrefixes:self.registeredPrefixes
completion:^(NSString *prefix, NSString *word, NSRange wordRange) {
if (prefix.length > 0 && word.length > 0) {
// Captures the detected symbol prefix
_foundPrefix = prefix;
// Removes the found prefix, or not.
_foundWord = [word substringFromIndex:prefix.length];
// Used later for replacing the detected range with a new string alias returned in -acceptAutoCompletionWithString:
_foundPrefixRange = NSMakeRange(wordRange.location, prefix.length);
[self slk_handleProcessedWord:word wordRange:wordRange];
}
else {
[self cancelAutoCompletion];
}
}];
}
- (void)slk_handleProcessedWord:(NSString *)word wordRange:(NSRange)wordRange
{
// Cancel auto-completion if the cursor is placed before the prefix
if (self.textView.selectedRange.location <= self.foundPrefixRange.location) {
return [self cancelAutoCompletion];
}
if (self.foundPrefix.length > 0) {
if (wordRange.length == 0 || wordRange.length != word.length) {
return [self cancelAutoCompletion];
}
if (word.length > 0) {
// If the prefix is still contained in the word, cancels
if ([self.foundWord rangeOfString:self.foundPrefix].location != NSNotFound) {
return [self cancelAutoCompletion];
}
}
else {
return [self cancelAutoCompletion];
}
}
else {
return [self cancelAutoCompletion];
}
[self didChangeAutoCompletionPrefix:self.foundPrefix andWord:self.foundWord];
}
- (void)slk_invalidateAutoCompletion
{
_foundPrefix = nil;
_foundWord = nil;
_foundPrefixRange = NSMakeRange(0,0);
[_autoCompletionView setContentOffset:CGPointZero];
}
- (void)slk_hideAutoCompletionViewIfNeeded
{
if (self.isAutoCompleting) {
[self showAutoCompletionView:NO];
}
}
#pragma mark - Text Caching
- (NSString *)keyForTextCaching
{
// No implementation here. Meant to be overriden in subclass.
return nil;
}
- (NSString *)slk_keyForPersistency
{
NSString *key = [self keyForTextCaching];
if (key == nil) {
return nil;
}
return [NSString stringWithFormat:@"%@.%@", SLKTextViewControllerDomain, key];
}
- (void)slk_reloadTextView
{
NSString *key = [self slk_keyForPersistency];
if (key == nil) {
return;
}
NSAttributedString *cachedAttributedText = [[NSAttributedString alloc] initWithString:@""];
id obj = [[NSUserDefaults standardUserDefaults] objectForKey:key];
if (obj) {
if ([obj isKindOfClass:[NSString class]]) {
cachedAttributedText = [[NSAttributedString alloc] initWithString:obj];
}
else if ([obj isKindOfClass:[NSData class]]) {
cachedAttributedText = [NSKeyedUnarchiver unarchiveObjectWithData:obj];
}
}
if (self.textView.attributedText.length == 0 || cachedAttributedText.length > 0) {
self.textView.attributedText = cachedAttributedText;
}
}
- (void)cacheTextView
{
[self slk_cacheAttributedTextToDisk:self.textView.attributedText];
}
- (void)clearCachedText
{
[self slk_cacheAttributedTextToDisk:nil];
}
- (void)slk_cacheAttributedTextToDisk:(NSAttributedString *)attributedText
{
NSString *key = [self slk_keyForPersistency];
if (!key || key.length == 0) {
return;
}
NSAttributedString *cachedAttributedText = [[NSAttributedString alloc] initWithString:@""];
id obj = [[NSUserDefaults standardUserDefaults] objectForKey:key];
if (obj) {
if ([obj isKindOfClass:[NSString class]]) {
cachedAttributedText = [[NSAttributedString alloc] initWithString:obj];
}
else if ([obj isKindOfClass:[NSData class]]) {
cachedAttributedText = [NSKeyedUnarchiver unarchiveObjectWithData:obj];
}
}
// Caches text only if its a valid string and not already cached
if (attributedText.length > 0 && ![attributedText isEqualToAttributedString:cachedAttributedText]) {
NSData *data = [NSKeyedArchiver archivedDataWithRootObject:attributedText];
[[NSUserDefaults standardUserDefaults] setObject:data forKey:key];
}
// Clears cache only if it exists
else if (attributedText.length == 0 && cachedAttributedText.length > 0) {
[[NSUserDefaults standardUserDefaults] removeObjectForKey:key];
}
else {
// Skips so it doesn't hit 'synchronize' unnecessarily
return;
}
[[NSUserDefaults standardUserDefaults] synchronize];
}
- (void)slk_cacheTextToDisk:(NSString *)text
{
NSString *key = [self slk_keyForPersistency];
if (!key || key.length == 0) {
return;
}
NSAttributedString *attributedText = [[NSAttributedString alloc] initWithString:text];
[self slk_cacheAttributedTextToDisk:attributedText];
}
+ (void)clearAllCachedText
{
NSMutableArray *cachedKeys = [NSMutableArray new];
for (NSString *key in [[[NSUserDefaults standardUserDefaults] dictionaryRepresentation] allKeys]) {
if ([key rangeOfString:SLKTextViewControllerDomain].location != NSNotFound) {
[cachedKeys addObject:key];
}
}
if (cachedKeys.count == 0) {
return;
}
for (NSString *cachedKey in cachedKeys) {
[[NSUserDefaults standardUserDefaults] removeObjectForKey:cachedKey];
}
[[NSUserDefaults standardUserDefaults] synchronize];
}
#pragma mark - Customization
- (void)registerClassForTextView:(Class)aClass
{
if (aClass == nil) {
return;
}
NSAssert([aClass isSubclassOfClass:[SLKTextView class]], @"The registered class is invalid, it must be a subclass of SLKTextView.");
self.textViewClass = aClass;
}
- (void)registerClassForTypingIndicatorView:(Class)aClass
{
if (aClass == nil) {
return;
}
NSAssert([aClass isSubclassOfClass:[UIView class]], @"The registered class is invalid, it must be a subclass of UIView.");
self.typingIndicatorViewClass = aClass;
}
- (void)adjustBottomMargin:(CGFloat)margin {
_bottomMargin = margin;
[self slk_updateViewConstraints];
}
#pragma mark - UITextViewDelegate Methods
- (BOOL)textView:(SLKTextView *)textView shouldChangeTextInRange:(NSRange)range replacementText:(NSString *)text
{
if (![textView isKindOfClass:[SLKTextView class]]) {
return YES;
}
BOOL newWordInserted = ([text rangeOfCharacterFromSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]].location != NSNotFound);
// Records text for undo for every new word
if (newWordInserted) {
[textView slk_prepareForUndo:@"Word Change"];
}
// Detects double spacebar tapping, to replace the default "." insert with a formatting symbol, if needed.
if (textView.isFormattingEnabled && range.location > 0 && text.length > 0 &&
[[NSCharacterSet whitespaceCharacterSet] characterIsMember:[text characterAtIndex:0]] &&
[[NSCharacterSet whitespaceCharacterSet] characterIsMember:[textView.text characterAtIndex:range.location - 1]]) {
BOOL shouldChange = YES;
// Since we are moving 2 characters to the left, we need for to make sure that the string's lenght,
// before the caret position, is higher than 2.
if ([textView.text substringToIndex:textView.selectedRange.location].length < 2) {
return YES;
}
NSRange wordRange = range;
wordRange.location -= 2; // minus the white space added with the double space bar tapping
if (wordRange.location == NSNotFound) {
return YES;
}
NSArray *symbols = textView.registeredSymbols;
NSMutableCharacterSet *invalidCharacters = [NSMutableCharacterSet new];
[invalidCharacters formUnionWithCharacterSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]];
[invalidCharacters formUnionWithCharacterSet:[NSCharacterSet punctuationCharacterSet]];
[invalidCharacters removeCharactersInString:[symbols componentsJoinedByString:@""]];
for (NSString *symbol in symbols) {
// Detects the closest registered symbol to the caret, from right to left
NSRange searchRange = NSMakeRange(0, wordRange.location);
NSRange prefixRange = [textView.text rangeOfString:symbol options:NSBackwardsSearch range:searchRange];
if (prefixRange.location == NSNotFound) {
continue;
}
NSRange nextCharRange = NSMakeRange(prefixRange.location+1, 1);
NSString *charAfterSymbol = [textView.text substringWithRange:nextCharRange];
if (prefixRange.location != NSNotFound && ![invalidCharacters characterIsMember:[charAfterSymbol characterAtIndex:0]]) {
if ([self textView:textView shouldInsertSuffixForFormattingWithSymbol:symbol prefixRange:prefixRange]) {
NSRange suffixRange;
[textView wordAtRange:wordRange rangeInText:&suffixRange];
// Skip if the detected word already has a suffix
if ([[textView.text substringWithRange:suffixRange] hasSuffix:symbol]) {
continue;
}
suffixRange.location += suffixRange.length;
suffixRange.length = 0;
NSString *lastCharacter = [textView.text substringWithRange:NSMakeRange(suffixRange.location, 1)];
// Checks if the last character was a line break, so we append the symbol in the next line too
if ([[NSCharacterSet newlineCharacterSet] characterIsMember:[lastCharacter characterAtIndex:0]]) {
suffixRange.location += 1;
}
[textView slk_insertText:symbol inRange:suffixRange];
shouldChange = NO;
// Reset the original cursor location +1 for the new character
NSRange adjustedCursorPosition = NSMakeRange(range.location + 1, 0);
textView.selectedRange = adjustedCursorPosition;
break; // exit
}
}
}
return shouldChange;
}
else if ([text isEqualToString:@"\n"]) {
//Detected break. Should insert new line break programatically instead.
[textView slk_insertNewLineBreak];
return NO;
}
else {
NSDictionary *userInfo = @{@"text": text, @"range": [NSValue valueWithRange:range]};
[[NSNotificationCenter defaultCenter] postNotificationName:SLKTextViewTextWillChangeNotification object:self.textView userInfo:userInfo];
return YES;
}
}
- (void)textViewDidChange:(SLKTextView *)textView
{
// Keep to avoid unnecessary crashes. Was meant to be overriden in subclass while calling super.
}
- (void)textViewDidChangeSelection:(SLKTextView *)textView
{
// Keep to avoid unnecessary crashes. Was meant to be overriden in subclass while calling super.
}
- (BOOL)textViewShouldBeginEditing:(SLKTextView *)textView
{
return YES;
}
- (BOOL)textViewShouldEndEditing:(SLKTextView *)textView
{
return YES;
}
- (void)textViewDidBeginEditing:(SLKTextView *)textView
{
// No implementation here. Meant to be overriden in subclass.
}
- (void)textViewDidEndEditing:(SLKTextView *)textView
{
// No implementation here. Meant to be overriden in subclass.
}
#pragma mark - SLKTextViewDelegate Methods
- (BOOL)textView:(SLKTextView *)textView shouldOfferFormattingForSymbol:(NSString *)symbol
{
return YES;
}
- (BOOL)textView:(SLKTextView *)textView shouldInsertSuffixForFormattingWithSymbol:(NSString *)symbol prefixRange:(NSRange)prefixRange
{
if (prefixRange.location > 0) {
NSRange previousCharRange = NSMakeRange(prefixRange.location-1, 1);
NSString *previousCharacter = [self.textView.text substringWithRange:previousCharRange];
// Only insert a suffix if the character before the prefix was a whitespace or a line break
if ([previousCharacter rangeOfCharacterFromSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]].location != NSNotFound) {
return YES;
}
else {
return NO;
}
}
return YES;
}
#pragma mark - UITableViewDataSource Methods
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
return 0;
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
return nil;
}
#pragma mark - UICollectionViewDataSource Methods
- (NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section;
{
return 0;
}
- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath
{
return nil;
}
#pragma mark - UIScrollViewDelegate Methods
- (BOOL)scrollViewShouldScrollToTop:(UIScrollView *)scrollView
{
if (!self.scrollViewProxy.scrollsToTop || self.keyboardStatus == SLKKeyboardStatusWillShow) {
return NO;
}
if (self.isInverted) {
[self.scrollViewProxy slk_scrollToBottomAnimated:YES];
return NO;
}
else {
return YES;
}
}
- (void)scrollViewDidEndDragging:(UIScrollView *)scrollView willDecelerate:(BOOL)decelerate
{
self.movingKeyboard = NO;
}
- (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView
{
self.movingKeyboard = NO;
}
- (void)scrollViewDidScroll:(UIScrollView *)scrollView
{
if ([scrollView isEqual:_autoCompletionView]) {
CGRect frame = self.autoCompletionHairline.frame;
frame.origin.y = scrollView.contentOffset.y;
self.autoCompletionHairline.frame = frame;
}
else {
if (!self.isMovingKeyboard) {
_scrollViewOffsetBeforeDragging = scrollView.contentOffset;
_keyboardHeightBeforeDragging = self.keyboardHC.constant;
}
}
}
#pragma mark - UIGestureRecognizerDelegate Methods
- (BOOL)gestureRecognizerShouldBegin:(UIGestureRecognizer *)gesture
{
if ([gesture isEqual:self.singleTapGesture]) {
return [self.textView isFirstResponder] && ![self ignoreTextInputbarAdjustment];
}
else if ([gesture isEqual:self.verticalPanGesture]) {
return self.keyboardPanningEnabled && ![self ignoreTextInputbarAdjustment];
}
else if ([gesture isEqual:self.backgroundTapGesture]) {
return YES;
}
return NO;
}
#pragma mark - UIAlertViewDelegate Methods
#ifndef __IPHONE_8_0
- (void)alertView:(UIAlertView *)alertView clickedButtonAtIndex:(NSInteger)buttonIndex
{
if (alertView.tag != kSLKAlertViewClearTextTag || buttonIndex == [alertView cancelButtonIndex] ) {
return;
}
// Clears the text but doesn't clear the undo manager
if (self.shakeToClearEnabled) {
[self.textView slk_clearText:NO];
}
}
#endif
#pragma mark - View Auto-Layout
- (void)slk_setupViewConstraints
{
NSDictionary *views = @{@"scrollView": self.scrollViewProxy,
@"backgroundView": self.autoCompletionBackgroundView,
@"autoCompletionView": self.autoCompletionView,
@"typingIndicatorView": self.typingIndicatorProxyView,
@"textInputbar": self.textInputbar
};
[self.view addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"V:|[scrollView(0@750)][typingIndicatorView(0)]-0@999-[textInputbar(0)]|" options:0 metrics:nil views:views]];
[self.view addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"V:|[backgroundView(>=0)][autoCompletionView(0)][typingIndicatorView]" options:0 metrics:nil views:views]];
[self.view addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"V:|-(>=0)-[autoCompletionView(0@750)][typingIndicatorView]" options:0 metrics:nil views:views]];
[self.view addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"H:|[scrollView]|" options:0 metrics:nil views:views]];
[self.view addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"H:|[backgroundView]|" options:0 metrics:nil views:views]];
[self.view addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"H:|[autoCompletionView]|" options:0 metrics:nil views:views]];
[self.view addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"H:|[typingIndicatorView]|" options:0 metrics:nil views:views]];
NSString *format = [NSString stringWithFormat:@"H:|-%f-[textInputbar]-%f-|", self.textInputBarLRC, self.textInputBarLRC];
[self.view addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:format options:0 metrics:nil views:views]];
self.scrollViewHC = [self.view slk_constraintForAttribute:NSLayoutAttributeHeight firstItem:self.scrollViewProxy secondItem:nil];
self.autoCompletionViewHC = [self.view slk_constraintForAttribute:NSLayoutAttributeHeight firstItem:self.autoCompletionView secondItem:nil];
self.typingIndicatorViewHC = [self.view slk_constraintForAttribute:NSLayoutAttributeHeight firstItem:self.typingIndicatorProxyView secondItem:nil];
self.textInputbarHC = [self.view slk_constraintForAttribute:NSLayoutAttributeHeight firstItem:self.textInputbar secondItem:nil];
self.keyboardHC = [self.view slk_constraintForAttribute:NSLayoutAttributeBottom firstItem:self.view secondItem:self.textInputbar];
[self slk_updateViewConstraints];
}
- (void)slk_updateViewConstraints
{
self.textInputbarHC.constant = _textInputbar.hidden ? 0 : self.textInputbar.minimumInputbarHeight;
self.scrollViewHC.constant = [self slk_appropriateScrollViewHeight];
self.keyboardHC.constant = [self slk_appropriateKeyboardHeightFromRect:CGRectNull];
if (_textInputbar.isEditing) {
self.textInputbarHC.constant += self.textInputbar.editorContentViewHeight;
}
[super updateViewConstraints];
}
- (void)slk_refreshViewContraints
{
self.textInputbarHC.constant = _textInputbar.hidden ? 0 : self.textInputbar.appropriateHeight;
self.scrollViewHC.constant = [self slk_appropriateScrollViewHeight];
if (_textInputbar.isEditing) {
self.textInputbarHC.constant += self.textInputbar.editorContentViewHeight;
}
[super updateViewConstraints];
}
#pragma mark - Keyboard Command registration
- (void)slk_registerKeyCommands
{
__weak typeof(self) weakSelf = self;
// Enter Key
[self.textView observeKeyInput:@"\r" modifiers:0 title:NSLocalizedString(@"Send/Accept", nil) completion:^(UIKeyCommand *keyCommand) {
[weakSelf didPressReturnKey:keyCommand];
}];
// Esc Key
[self.textView observeKeyInput:UIKeyInputEscape modifiers:0 title:NSLocalizedString(@"Dismiss", nil) completion:^(UIKeyCommand *keyCommand) {
[weakSelf didPressEscapeKey:keyCommand];
}];
// Up Arrow
[self.textView observeKeyInput:UIKeyInputUpArrow modifiers:0 title:nil completion:^(UIKeyCommand *keyCommand) {
[weakSelf didPressArrowKey:keyCommand];
}];
// Down Arrow
[self.textView observeKeyInput:UIKeyInputDownArrow modifiers:0 title:nil completion:^(UIKeyCommand *keyCommand) {
[weakSelf didPressArrowKey:keyCommand];
}];
}
- (NSArray *)keyCommands
{
// Important to keep this in, for backwards compatibility.
return @[];
}
#pragma mark - NSNotificationCenter registration
- (void)slk_registerNotifications
{
[self slk_unregisterNotifications];
NSNotificationCenter *notificationCenter = [NSNotificationCenter defaultCenter];
// Keyboard notifications
[notificationCenter addObserver:self selector:@selector(slk_willShowOrHideKeyboard:) name:UIKeyboardWillShowNotification object:nil];
[notificationCenter addObserver:self selector:@selector(slk_willShowOrHideKeyboard:) name:UIKeyboardWillHideNotification object:nil];
[notificationCenter addObserver:self selector:@selector(slk_didShowOrHideKeyboard:) name:UIKeyboardDidShowNotification object:nil];
[notificationCenter addObserver:self selector:@selector(slk_didShowOrHideKeyboard:) name:UIKeyboardDidHideNotification object:nil];
#if SLK_KEYBOARD_NOTIFICATION_DEBUG
[notificationCenter addObserver:self selector:@selector(slk_didPostSLKKeyboardNotification:) name:SLKKeyboardWillShowNotification object:nil];
[notificationCenter addObserver:self selector:@selector(slk_didPostSLKKeyboardNotification:) name:SLKKeyboardDidShowNotification object:nil];
[notificationCenter addObserver:self selector:@selector(slk_didPostSLKKeyboardNotification:) name:SLKKeyboardWillHideNotification object:nil];
[notificationCenter addObserver:self selector:@selector(slk_didPostSLKKeyboardNotification:) name:SLKKeyboardDidHideNotification object:nil];
#endif
// TextView notifications
[notificationCenter addObserver:self selector:@selector(slk_willChangeTextViewText:) name:SLKTextViewTextWillChangeNotification object:nil];
[notificationCenter addObserver:self selector:@selector(slk_didChangeTextViewText:) name:UITextViewTextDidChangeNotification object:nil];
[notificationCenter addObserver:self selector:@selector(slk_didChangeTextViewContentSize:) name:SLKTextViewContentSizeDidChangeNotification object:nil];
[notificationCenter addObserver:self selector:@selector(slk_didChangeTextViewSelectedRange:) name:SLKTextViewSelectedRangeDidChangeNotification object:nil];
[notificationCenter addObserver:self selector:@selector(slk_didChangeTextViewPasteboard:) name:SLKTextViewDidPasteItemNotification object:nil];
[notificationCenter addObserver:self selector:@selector(slk_didShakeTextView:) name:SLKTextViewDidShakeNotification object:nil];
// Application notifications
[notificationCenter addObserver:self selector:@selector(cacheTextView) name:UIApplicationWillTerminateNotification object:nil];
[notificationCenter addObserver:self selector:@selector(cacheTextView) name:UIApplicationDidEnterBackgroundNotification object:nil];
[notificationCenter addObserver:self selector:@selector(cacheTextView) name:UIApplicationDidReceiveMemoryWarningNotification object:nil];
}
- (void)slk_unregisterNotifications
{
NSNotificationCenter *notificationCenter = [NSNotificationCenter defaultCenter];
// Keyboard notifications
[notificationCenter removeObserver:self name:UIKeyboardWillShowNotification object:nil];
[notificationCenter removeObserver:self name:UIKeyboardWillHideNotification object:nil];
[notificationCenter removeObserver:self name:UIKeyboardDidShowNotification object:nil];
[notificationCenter removeObserver:self name:UIKeyboardDidHideNotification object:nil];
#if SLK_KEYBOARD_NOTIFICATION_DEBUG
[notificationCenter removeObserver:self name:SLKKeyboardWillShowNotification object:nil];
[notificationCenter removeObserver:self name:SLKKeyboardDidShowNotification object:nil];
[notificationCenter removeObserver:self name:SLKKeyboardWillHideNotification object:nil];
[notificationCenter removeObserver:self name:SLKKeyboardDidHideNotification object:nil];
#endif
// TextView notifications
[notificationCenter removeObserver:self name:UITextViewTextDidBeginEditingNotification object:nil];
[notificationCenter removeObserver:self name:UITextViewTextDidEndEditingNotification object:nil];
[notificationCenter removeObserver:self name:SLKTextViewTextWillChangeNotification object:nil];
[notificationCenter removeObserver:self name:UITextViewTextDidChangeNotification object:nil];
[notificationCenter removeObserver:self name:SLKTextViewContentSizeDidChangeNotification object:nil];
[notificationCenter removeObserver:self name:SLKTextViewSelectedRangeDidChangeNotification object:nil];
[notificationCenter removeObserver:self name:SLKTextViewDidPasteItemNotification object:nil];
[notificationCenter removeObserver:self name:SLKTextViewDidShakeNotification object:nil];
// Application notifications
[notificationCenter removeObserver:self name:UIApplicationWillTerminateNotification object:nil];
[notificationCenter removeObserver:self name:UIApplicationDidEnterBackgroundNotification object:nil];
[notificationCenter removeObserver:self name:UIApplicationDidReceiveMemoryWarningNotification object:nil];
}
#pragma mark - View Auto-Rotation
#ifdef __IPHONE_8_0
- (void)willTransitionToTraitCollection:(UITraitCollection *)newCollection withTransitionCoordinator:(id <UIViewControllerTransitionCoordinator>)coordinator
{
[super willTransitionToTraitCollection:newCollection withTransitionCoordinator:coordinator];
}
- (void)viewWillTransitionToSize:(CGSize)size withTransitionCoordinator:(id<UIViewControllerTransitionCoordinator>)coordinator
{
[self slk_prepareForInterfaceTransitionWithDuration:coordinator.transitionDuration];
[super viewWillTransitionToSize:size withTransitionCoordinator:coordinator];
}
#else
- (void)willRotateToInterfaceOrientation:(UIInterfaceOrientation)toInterfaceOrientation duration:(NSTimeInterval)duration
{
if ([self respondsToSelector:@selector(viewWillTransitionToSize:withTransitionCoordinator:)]) {
return;
}
[self slk_prepareForInterfaceTransitionWithDuration:duration];
}
#endif
#ifdef __IPHONE_9_0
- (UIInterfaceOrientationMask)supportedInterfaceOrientations
#else
- (NSUInteger)supportedInterfaceOrientations
#endif
{
return UIInterfaceOrientationMaskAll;
}
- (BOOL)shouldAutorotate
{
return YES;
}
#pragma mark - View lifeterm
- (void)didReceiveMemoryWarning
{
[super didReceiveMemoryWarning];
}
- (void)dealloc
{
[self slk_unregisterNotifications];
[_typingIndicatorProxyView removeObserver:self forKeyPath:@"visible"];
}
@end