You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@usergrid.apache.org by sn...@apache.org on 2016/02/18 16:52:55 UTC
[47/89] [partial] usergrid git commit: Initial commit of the Swift
SDK.
http://git-wip-us.apache.org/repos/asf/usergrid/blob/7442c881/sdks/swift/Samples/ActivityFeed/Pods/SlackTextViewController/Source/SLKTextViewController.m
----------------------------------------------------------------------
diff --git a/sdks/swift/Samples/ActivityFeed/Pods/SlackTextViewController/Source/SLKTextViewController.m b/sdks/swift/Samples/ActivityFeed/Pods/SlackTextViewController/Source/SLKTextViewController.m
new file mode 100644
index 0000000..5febfcb
--- /dev/null
+++ b/sdks/swift/Samples/ActivityFeed/Pods/SlackTextViewController/Source/SLKTextViewController.m
@@ -0,0 +1,2392 @@
+//
+// Copyright 2014 Slack Technologies, Inc.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+
+#import "SLKTextViewController.h"
+#import "SLKInputAccessoryView.h"
+
+#import "UIResponder+SLKAdditions.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;
+
+// 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;
+
+// YES if the user is moving the keyboard with a gesture
+@property (nonatomic, assign, getter = isMovingKeyboard) BOOL movingKeyboard;
+
+// The current keyboard status (hidden, showing, etc.)
+@property (nonatomic) SLKKeyboardStatus keyboardStatus;
+
+// 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;
+
+#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.");
+
+ 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.");
+
+ 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.");
+
+ 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.");
+
+ 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.automaticallyAdjustsScrollViewInsets = YES;
+ self.extendedLayoutIncludesOpaqueBars = YES;
+}
+
+
+#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.typingIndicatorProxyView];
+ [self.view addSubview:self.textInputbar];
+
+ [self slk_setupViewConstraints];
+}
+
+- (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;
+
+ // Caches the text before it's too late!
+ [self slk_cacheTextView];
+}
+
+- (void)viewDidDisappear:(BOOL)animated
+{
+ [super viewDidDisappear:animated];
+}
+
+- (void)viewWillLayoutSubviews
+{
+ [super viewWillLayoutSubviews];
+
+ [self slk_adjustContentConfigurationIfNeeded];
+}
+
+- (void)viewDidLayoutSubviews
+{
+ [super viewDidLayoutSubviews];
+}
+
+
+#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.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;
+}
+
+- (SLKTextInputbar *)textInputbar
+{
+ if (!_textInputbar) {
+ _textInputbar = [[SLKTextInputbar alloc] initWithTextViewClass:self.textViewClass];
+ _textInputbar.translatesAutoresizingMaskIntoConstraints = NO;
+ _textInputbar.controller = self;
+
+ [_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;
+}
+
+- (SLKTextView *)textView
+{
+ return self.textInputbar.textView;
+}
+
+- (UIButton *)leftButton
+{
+ return self.textInputbar.leftButton;
+}
+
+- (UIButton *)rightButton
+{
+ return self.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;
+ }
+
+ 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) {
+ if (self.tabBarController) {
+ return CGRectGetHeight(self.tabBarController.tabBar.frame);
+ }
+ }
+
+ return 0.0;
+}
+
+- (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)setInverted:(BOOL)inverted
+{
+ if (_inverted == inverted) {
+ return;
+ }
+
+ _inverted = inverted;
+
+ self.scrollViewProxy.transform = inverted ? CGAffineTransformMake(1, 0, 0, -1, 0, 0) : CGAffineTransformIdentity;
+}
+
+- (BOOL)slk_updateKeyboardStatus:(SLKKeyboardStatus)status
+{
+ // Skips if trying to update the same status
+ if (_keyboardStatus == status) {
+ return NO;
+ }
+
+ // Skips illogical conditions
+ if ([self slk_isIllogicalKeyboardStatus:status]) {
+ return NO;
+ }
+
+ _keyboardStatus = status;
+
+ [self didChangeKeyboardStatus:status];
+
+ return YES;
+}
+
+
+#pragma mark - Public & Subclassable Methods
+
+- (void)presentKeyboard:(BOOL)animated
+{
+ // 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
+{
+ // 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.textInputbarHidden) {
+ return;
+ }
+
+ CGFloat inputbarHeight = self.textInputbar.appropriateHeight;
+
+ self.textInputbar.rightButton.enabled = [self canPressRightButton];
+ self.textInputbar.editorRightButton.enabled = [self canPressRightButton];
+
+ if (inputbarHeight != self.textInputbarHC.constant)
+ {
+ self.textInputbarHC.constant = inputbarHeight;
+ self.scrollViewHC.constant = [self slk_appropriateScrollViewHeight];
+
+ if (animated) {
+
+ BOOL bounces = self.bounces && [self.textView isFirstResponder];
+
+ [self.view slk_animateLayoutIfNeededWithBounce:bounces
+ options:UIViewAnimationOptionCurveEaseInOut|UIViewAnimationOptionLayoutSubviews|UIViewAnimationOptionBeginFromCurrentState
+ animations:^{
+ if (self.textInputbar.isEditing) {
+ [self.textView slk_scrollToCaretPositonAnimated:NO];
+ }
+ }];
+ }
+ else {
+ [self.view layoutIfNeeded];
+ }
+ }
+
+ // Toggles auto-correction if requiered
+ [self slk_enableTypingSuggestionIfNeeded];
+}
+
+- (void)textSelectionDidChange
+{
+ // The text view must be first responder
+ if (![self.textView isFirstResponder]) {
+ return;
+ }
+
+ // Skips there is a real text selection
+ if (self.textView.isTrackpadEnabled) {
+ return;
+ }
+
+ if (self.textView.selectedRange.length > 0) {
+ if (self.isAutoCompleting) {
+ [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 && ![self.textInputbar limitExceeded]) {
+ return YES;
+ }
+
+ return NO;
+}
+
+- (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];
+ }
+
+ // Clears cache
+ [self clearCachedText];
+}
+
+- (void)editText:(NSString *)text
+{
+ if (![self.textInputbar canEditText:text]) {
+ return;
+ }
+
+ // Caches the current text, in case the user cancels the edition
+ [self slk_cacheTextToDisk:self.textView.text];
+
+ [self.textInputbar beginTextEditing];
+
+ // Setting the text after calling -beginTextEditing is safer
+ [self.textView setText:text];
+
+ [self.textView slk_scrollToCaretPositonAnimated:YES];
+
+ // Brings up the keyboard if needed
+ [self presentKeyboard:YES];
+}
+
+- (void)didCommitTextEditing:(id)sender
+{
+ if (!self.textInputbar.isEditing) {
+ return;
+ }
+
+ [self.textInputbar endTextEdition];
+
+ // Clears the text and but not the undo manager
+ [self.textView slk_clearText:NO];
+}
+
+- (void)didCancelTextEditing:(id)sender
+{
+ if (!self.textInputbar.isEditing) {
+ return;
+ }
+
+ [self.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 (self.textInputbar.isEditing || self.isAutoCompleting) {
+ return NO;
+ }
+
+ // Don't show if the content offset is not at top (when inverted) or at bottom (when not inverted)
+ if ((self.isInverted && ![self.scrollViewProxy slk_isAtTop]) || (!self.isInverted && ![self.scrollViewProxy slk_isAtBottom])) {
+ 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;
+ }
+
+ _textInputbarHidden = hidden;
+
+ __weak typeof(self) weakSelf = self;
+
+ void (^animations)() = ^void(){
+
+ weakSelf.textInputbarHC.constant = hidden ? 0 : weakSelf.textInputbar.appropriateHeight;
+
+ [weakSelf.view layoutIfNeeded];
+ };
+
+ 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 = [self.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.
+ if (![self.textView isFirstResponder] || (self.keyboardHC.constant == 0 && 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:self.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 = [self.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(self.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;
+
+ // 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 ([self.textView.textInputMode.primaryLanguage isEqualToString:@"ja-JP"]) {
+ return;
+ }
+
+ // During text autocompletion, the iOS 8 QuickType bar is hidden and auto-correction and spell checking are disabled.
+ [self.textView setTypingSuggestionEnabled:enable];
+}
+
+- (void)slk_dismissTextInputbarIfNeeded
+{
+ if (self.keyboardHC.constant == 0) {
+ return;
+ }
+
+ self.keyboardHC.constant = 0.0;
+ 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:(id)sender
+{
+ if (self.textInputbar.isEditing) {
+ [self didCommitTextEditing:sender];
+ }
+ else {
+ [self slk_performRightAction];
+ }
+}
+
+- (void)didPressEscapeKey:(id)sender
+{
+ if (self.isAutoCompleting) {
+ [self cancelAutoCompletion];
+ }
+ else if (self.textInputbar.isEditing) {
+ [self didCancelTextEditing:sender];
+ }
+
+ if ([self ignoreTextInputbarAdjustment] || ([self.textView isFirstResponder] && self.keyboardHC.constant == 0)) {
+ return;
+ }
+
+ [self dismissKeyboard:YES];
+}
+
+- (void)didPressArrowKey:(id)sender
+{
+ [self.textView didPressAnyArrowKey:sender];
+}
+
+
+#pragma mark - Notification Events
+
+- (void)slk_willShowOrHideKeyboard:(NSNotification *)notification
+{
+ // Skips if the view isn't visible.
+ if (!self.view.window) {
+ 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;
+ }
+
+ SLKKeyboardStatus status = [self slk_keyboardStatusForNotification:notification];
+
+ // 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];
+ }
+
+ // Skips this 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 (![self.textView isFirstResponder]) {
+ // Detect the current first responder. If there is no first responder, we should just ignore these notifications.
+ UIResponder *currentResponder = [UIResponder slk_currentFirstResponder];
+
+ if (!currentResponder) {
+ return;
+ }
+ else if (![self forceTextInputbarAdjustmentForResponder:currentResponder]) {
+ return [self slk_dismissTextInputbarIfNeeded];
+ }
+ }
+
+ // Programatically stops scrolling before updating the view constraints (to avoid scrolling glitch).
+ if (status == SLKKeyboardStatusWillShow) {
+ [self.scrollViewProxy slk_stopScrolling];
+ }
+
+ // Hides the auto-completion view if the keyboard is being dismissed.
+ if (![self.textView isFirstResponder] || status == SLKKeyboardStatusWillHide) {
+ [self slk_hideAutoCompletionViewIfNeeded];
+ }
+
+ // Updates the height constraints' constants
+ self.keyboardHC.constant = [self slk_appropriateKeyboardHeightFromNotification:notification];
+ self.scrollViewHC.constant = [self slk_appropriateScrollViewHeight];
+
+
+ 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) {
+ if (self.isInverted) {
+ [self.scrollViewProxy slk_scrollToTopAnimated:YES];
+ }
+ else {
+ [self.scrollViewProxy 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.
+ if (!CGRectEqualToRect(beginFrame, endFrame))
+ {
+ // 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
+{
+ // Skips if the view isn't visible
+ if (!self.view.window) {
+ 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;
+ }
+
+ SLKKeyboardStatus status = [self slk_keyboardStatusForNotification:notification];
+
+ // 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] || !self.textView.window) {
+ return;
+ }
+
+ [self textWillUpdate];
+}
+
+- (void)slk_didChangeTextViewText:(NSNotification *)notification
+{
+ // Skips this it's not the expected textView.
+ if (![notification.object isEqual:self.textView] || !self.textView.window) {
+ 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] || !self.textView.window) {
+ 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] || !self.textView.window) {
+ 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 isKindOfClass:[NSDictionary class]]) {
+ [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> *)typingIndicatorView
+{
+ // Skips if the typing indicator should not show. Ignores the checking if it's trying to hide.
+ if (![self canShowTypingIndicator] && typingIndicatorView.isVisible) {
+ return;
+ }
+
+ CGFloat systemLayoutSizeHeight = [typingIndicatorView systemLayoutSizeFittingSize:UILayoutFittingCompressedSize].height;
+ CGFloat height = typingIndicatorView.isVisible ? systemLayoutSizeHeight : 0.0;
+
+ self.typingIndicatorViewHC.constant = height;
+ self.scrollViewHC.constant -= height;
+
+ if (typingIndicatorView.isVisible) {
+ typingIndicatorView.hidden = NO;
+ }
+
+ [self.view slk_animateLayoutIfNeededWithBounce:self.bounces
+ options:UIViewAnimationOptionCurveEaseInOut
+ animations:NULL
+ completion:^(BOOL finished) {
+ if (!typingIndicatorView.isVisible) {
+ typingIndicatorView.hidden = YES;
+ }
+ }];
+}
+
+- (void)slk_willTerminateApplication:(NSNotification *)notification
+{
+ // Caches the text before it's too late!
+ if (self.isViewVisible) {
+ [self slk_cacheTextView];
+ }
+}
+
+
+#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 *)prefixes
+{
+ NSMutableArray *array = [NSMutableArray arrayWithArray:self.registeredPrefixes];
+
+ for (NSString *prefix in prefixes) {
+ // Skips if the prefix is not a valid string
+ if (![prefix isKindOfClass:[NSString class]] || prefix.length == 0) {
+ continue;
+ }
+
+ // Adds the prefix if not contained already
+ if (![array containsObject:prefix]) {
+ [array addObject:prefix];
+ }
+ }
+
+ if (_registeredPrefixes) {
+ _registeredPrefixes = nil;
+ }
+
+ _registeredPrefixes = [[NSArray alloc] initWithArray:array];
+}
+
+- (void)didChangeAutoCompletionPrefix:(NSString *)prefix andWord:(NSString *)word
+{
+ // No implementation here. Meant to be overriden in subclass.
+}
+
+- (BOOL)canShowAutoCompletion
+{
+ // Let's keep this around for a bit, for backwards compatibility.
+ return NO;
+}
+
+- (void)showAutoCompletionView:(BOOL)show
+{
+ // Reloads the tableview before showing/hiding
+ if (show) {
+ [self.autoCompletionView reloadData];
+ }
+
+ self.autoCompleting = show;
+
+ // Toggles auto-correction if requiered
+ [self slk_enableTypingSuggestionIfNeeded];
+
+ CGFloat viewHeight = show ? [self heightForAutoCompletionView] : 0.0;
+
+ 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)acceptAutoCompletionWithString:(NSString *)string
+{
+ [self acceptAutoCompletionWithString:string keepPrefix:YES];
+}
+
+- (void)acceptAutoCompletionWithString:(NSString *)string keepPrefix:(BOOL)keepPrefix
+{
+ if (string.length == 0) {
+ return;
+ }
+
+ SLKTextView *textView = self.textView;
+
+ 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 = [textView slk_insertText:string inRange:range];
+
+ textView.selectedRange = NSMakeRange(insertionRange.location, 0);
+
+ [self cancelAutoCompletion];
+
+ [textView slk_scrollToCaretPositonAnimated:NO];
+}
+
+- (void)cancelAutoCompletion
+{
+ [self slk_invalidateAutoCompletion];
+ [self slk_hideAutoCompletionViewIfNeeded];
+}
+
+- (void)slk_processTextForAutoCompletion
+{
+ if (self.isTransitioning) {
+ return;
+ }
+
+ // Avoids text processing for auto-completion if the registered prefix list is empty.
+ if (self.registeredPrefixes.count == 0) {
+ return;
+ }
+
+ NSString *text = self.textView.text;
+
+ // Skip, when there is no text to process
+ if (text.length == 0) {
+ return [self cancelAutoCompletion];
+ }
+
+ NSRange range;
+ NSString *word = [self.textView slk_wordAtCaretRange:&range];
+
+ [self slk_invalidateAutoCompletion];
+
+ if (word.length > 0) {
+
+ for (NSString *prefix in self.registeredPrefixes) {
+ if ([word hasPrefix:prefix]) {
+ // Captures the detected symbol prefix
+ _foundPrefix = prefix;
+
+ // Used later for replacing the detected range with a new string alias returned in -acceptAutoCompletionWithString:
+ _foundPrefixRange = NSMakeRange(range.location, prefix.length);
+ }
+ }
+ }
+
+ [self slk_handleProcessedWord:word range:range];
+}
+
+- (void)slk_handleProcessedWord:(NSString *)word range:(NSRange)range
+{
+ // 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 (range.length == 0 || range.length != word.length) {
+ return [self cancelAutoCompletion];
+ }
+
+ if (word.length > 0) {
+ // Removes the found prefix
+ _foundWord = [word substringFromIndex:self.foundPrefix.length];
+
+ // 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);
+
+ [self.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;
+ }
+ NSString *cachedText = [[NSUserDefaults standardUserDefaults] objectForKey:key];
+
+ if (self.textView.text.length == 0 || cachedText.length > 0) {
+ self.textView.text = cachedText;
+ }
+}
+
+- (void)slk_cacheTextView
+{
+ [self slk_cacheTextToDisk:self.textView.text];
+}
+
+- (void)clearCachedText
+{
+ [self slk_cacheTextToDisk:nil];
+}
+
+- (void)slk_cacheTextToDisk:(NSString *)text
+{
+ NSString *key = [self slk_keyForPersistency];
+
+ if (!key || key.length == 0) {
+ return;
+ }
+
+ NSString *cachedText = [[NSUserDefaults standardUserDefaults] objectForKey:key];
+
+ // Caches text only if its a valid string and not already cached
+ if (text.length > 0 && ![text isEqualToString:cachedText]) {
+ [[NSUserDefaults standardUserDefaults] setObject:text forKey:key];
+ }
+ // Clears cache only if it exists
+ else if (text.length == 0 && cachedText.length > 0) {
+ [[NSUserDefaults standardUserDefaults] removeObjectForKey:key];
+ }
+ else {
+ // Skips so it doesn't hit 'synchronize' unnecessarily
+ return;
+ }
+
+ [[NSUserDefaults standardUserDefaults] synchronize];
+}
+
++ (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;
+}
+
+
+#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);
+
+ // It should not change if auto-completion is active and trying to replace with an auto-correction suggested text.
+ if (self.isAutoCompleting && text.length > 1) {
+ return NO;
+ }
+
+ // 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.autoCompleteFormatting && range.location > 0 && [text length] > 0 &&
+ [[NSCharacterSet whitespaceCharacterSet] characterIsMember:[text characterAtIndex:0]] &&
+ [[NSCharacterSet whitespaceCharacterSet] characterIsMember:[textView.text characterAtIndex:range.location - 1]]) {
+
+ BOOL shouldChange = YES;
+
+ NSRange wordRange = range;
+ wordRange.location -= 2; // minus the white space added with the double space bar tapping
+
+ 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 slk_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;
+
+ 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:self.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];
+ }
+
+ return NO;
+}
+
+
+#pragma mark - UIAlertViewDelegate Methods
+
+- (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];
+ }
+}
+
+
+#pragma mark - View Auto-Layout
+
+- (void)slk_setupViewConstraints
+{
+ NSDictionary *views = @{@"scrollView": self.scrollViewProxy,
+ @"autoCompletionView": self.autoCompletionView,
+ @"typingIndicatorView": self.typingIndicatorProxyView,
+ @"textInputbar": self.textInputbar,
+ };
+
+ [self.view addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"V:|[scrollView(0@750)][autoCompletionView(0@750)][typingIndicatorView(0)]-0@999-[textInputbar(0)]-0-|" 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:|[autoCompletionView]|" options:0 metrics:nil views:views]];
+ [self.view addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"H:|[typingIndicatorView]|" options:0 metrics:nil views:views]];
+ [self.view addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"H:|[textInputbar]|" 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 = self.textInputbar.minimumInputbarHeight;
+ self.scrollViewHC.constant = [self slk_appropriateScrollViewHeight];
+ self.keyboardHC.constant = [self slk_appropriateKeyboardHeightFromRect:CGRectNull];
+
+ if (self.textInputbar.isEditing) {
+ self.textInputbarHC.constant += self.textInputbar.editorContentViewHeight;
+ }
+
+ [super updateViewConstraints];
+}
+
+
+#pragma mark - External Keyboard Support
+
+- (NSArray *)keyCommands
+{
+ NSMutableArray *keyboardCommands = [NSMutableArray new];
+
+ [keyboardCommands addObject:[self slk_returnKeyCommand]];
+ [keyboardCommands addObject:[self slk_escKeyCommand]];
+ [keyboardCommands addObject:[self slk_arrowKeyCommand:UIKeyInputUpArrow]];
+ [keyboardCommands addObject:[self slk_arrowKeyCommand:UIKeyInputDownArrow]];
+
+ return keyboardCommands;
+}
+
+- (UIKeyCommand *)slk_returnKeyCommand
+{
+ UIKeyCommand *command = [UIKeyCommand keyCommandWithInput:@"\r" modifierFlags:0 action:@selector(didPressReturnKey:)];
+
+#ifdef __IPHONE_9_0
+ if ([UIKeyCommand respondsToSelector:@selector(keyCommandWithInput:modifierFlags:action:discoverabilityTitle:)] ) {
+ // Only available since iOS 9
+ if (self.textInputbar.isEditing) {
+ command.discoverabilityTitle = [self.textInputbar.editorRightButton titleForState:UIControlStateNormal] ? : NSLocalizedString(@"Commit Editing", nil);
+ }
+ else if (self.textView.text.length > 0) {
+ command.discoverabilityTitle = [self.rightButton titleForState:UIControlStateNormal] ? : NSLocalizedString(@"Send", nil);
+ }
+ }
+#endif
+
+ return command;
+}
+
+- (UIKeyCommand *)slk_escKeyCommand
+{
+ UIKeyCommand *command = [UIKeyCommand keyCommandWithInput:UIKeyInputEscape modifierFlags:0 action:@selector(didPressEscapeKey:)];
+
+#ifdef __IPHONE_9_0
+ if ([UIKeyCommand respondsToSelector:@selector(keyCommandWithInput:modifierFlags:action:discoverabilityTitle:)] ) {
+ // Only available since iOS 9
+ if (self.isAutoCompleting) {
+ command.discoverabilityTitle = NSLocalizedString(@"Exit Auto-Completion", nil);
+ }
+ else if (self.textInputbar.isEditing) {
+ command.discoverabilityTitle = [self.textInputbar.editorRightButton titleForState:UIControlStateNormal] ? : NSLocalizedString(@"Exit Editing", nil);
+ }
+ else if (!self.isExternalKeyboardDetected && self.keyboardHC.constant != 0) {
+ command.discoverabilityTitle = NSLocalizedString(@"Hide Keyboard", nil);
+ }
+ }
+#endif
+
+ return command;
+}
+
+- (UIKeyCommand *)slk_arrowKeyCommand:(NSString *)inputUpArrow
+{
+ UIKeyCommand *command = [UIKeyCommand keyCommandWithInput:inputUpArrow modifierFlags:0 action:@selector(didPressArrowKey:)];
+
+#ifdef __IPHONE_9_0
+ // Only available since iOS 9
+ if ([UIKeyCommand respondsToSelector:@selector(keyCommandWithInput:modifierFlags:action:discoverabilityTitle:)] ) {
+ if ([inputUpArrow isEqualToString:UIKeyInputUpArrow]) {
+ command.discoverabilityTitle = NSLocalizedString(@"Move Up", nil);
+ }
+ if ([inputUpArrow isEqualToString:UIKeyInputDownArrow]) {
+ command.discoverabilityTitle = NSLocalizedString(@"Move Down", nil);
+ }
+ }
+#endif
+
+ return command;
+}
+
+
+#pragma mark - NSNotificationCenter register/unregister
+
+- (void)slk_registerNotifications
+{
+ [self slk_unregisterNotifications];
+
+ // Keyboard notifications
+ [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(slk_willShowOrHideKeyboard:) name:UIKeyboardWillShowNotification object:nil];
+ [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(slk_willShowOrHideKeyboard:) name:UIKeyboardWillHideNotification object:nil];
+ [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(slk_didShowOrHideKeyboard:) name:UIKeyboardDidShowNotification object:nil];
+ [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(slk_didShowOrHideKeyboard:) name:UIKeyboardDidHideNotification object:nil];
+
+#if SLK_KEYBOARD_NOTIFICATION_DEBUG
+ [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(slk_didPostSLKKeyboardNotification:) name:SLKKeyboardWillShowNotification object:nil];
+ [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(slk_didPostSLKKeyboardNotification:) name:SLKKeyboardDidShowNotification object:nil];
+ [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(slk_didPostSLKKeyboardNotification:) name:SLKKeyboardWillHideNotification object:nil];
+ [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(slk_didPostSLKKeyboardNotification:) name:SLKKeyboardDidHideNotification object:nil];
+#endif
+
+ // TextView notifications
+ [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(slk_willChangeTextViewText:) name:SLKTextViewTextWillChangeNotification object:nil];
+ [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(slk_didChangeTextViewText:) name:UITextViewTextDidChangeNotification object:nil];
+ [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(slk_didChangeTextViewContentSize:) name:SLKTextViewContentSizeDidChangeNotification object:nil];
+ [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(slk_didChangeTextViewSelectedRange:) name:SLKTextViewSelectedRangeDidChangeNotification object:nil];
+
+ [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(slk_didChangeTextViewPasteboard:) name:SLKTextViewDidPasteItemNotification object:nil];
+ [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(slk_didShakeTextView:) name:SLKTextViewDidShakeNotification object:nil];
+
+ // Application notifications
+ [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(slk_willTerminateApplication:) name:UIApplicationWillTerminateNotification object:nil];
+ [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(slk_willTerminateApplication:) name:UIApplicationDidReceiveMemoryWarningNotification object:nil];
+}
+
+- (void)slk_unregisterNotifications
+{
+ // Keyboard notifications
+ [[NSNotificationCenter defaultCenter] removeObserver:self name:UIKeyboardWillShowNotification object:nil];
+ [[NSNotificationCenter defaultCenter] removeObserver:self name:UIKeyboardWillHideNotification object:nil];
+ [[NSNotificationCenter defaultCenter] removeObserver:self name:UIKeyboardDidShowNotification object:nil];
+ [[NSNotificationCenter defaultCenter] removeObserver:self name:UIKeyboardDidHideNotification object:nil];
+
+#if SLK_KEYBOARD_NOTIFICATION_DEBUG
+ [[NSNotificationCenter defaultCenter] removeObserver:self name:SLKKeyboardWillShowNotification object:nil];
+ [[NSNotificationCenter defaultCenter] removeObserver:self name:SLKKeyboardDidShowNotification object:nil];
+ [[NSNotificationCenter defaultCenter] removeObserver:self name:SLKKeyboardWillHideNotification object:nil];
+ [[NSNotificationCenter defaultCenter] removeObserver:self name:SLKKeyboardDidHideNotification object:nil];
+#endif
+
+ // TextView notifications
+ [[NSNotificationCenter defaultCenter] removeObserver:self name:UITextViewTextDidBeginEditingNotification object:nil];
+ [[NSNotificationCenter defaultCenter] removeObserver:self name:UITextViewTextDidEndEditingNotification object:nil];
+ [[NSNotificationCenter defaultCenter] removeObserver:self name:SLKTextViewTextWillChangeNotification object:nil];
+ [[NSNotificationCenter defaultCenter] removeObserver:self name:UITextViewTextDidChangeNotification object:nil];
+ [[NSNotificationCenter defaultCenter] removeObserver:self name:SLKTextViewContentSizeDidChangeNotification object:nil];
+ [[NSNotificationCenter defaultCenter] removeObserver:self name:SLKTextViewSelectedRangeDidChangeNotification object:nil];
+ [[NSNotificationCenter defaultCenter] removeObserver:self name:SLKTextViewDidPasteItemNotification object:nil];
+ [[NSNotificationCenter defaultCenter] removeObserver:self name:SLKTextViewDidShakeNotification object:nil];
+
+ // Application notifications
+ [[NSNotificationCenter defaultCenter] removeObserver:self name:UIApplicationWillTerminateNotification object:nil];
+ [[NSNotificationCenter defaultCenter] 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
+{
+ _tableView.delegate = nil;
+ _tableView.dataSource = nil;
+ _tableView = nil;
+
+ _collectionView.delegate = nil;
+ _collectionView.dataSource = nil;
+ _collectionView = nil;
+
+ _scrollView = nil;
+
+ _autoCompletionView.delegate = nil;
+ _autoCompletionView.dataSource = nil;
+ _autoCompletionView = nil;
+
+ _textInputbar.textView.delegate = nil;
+ _textInputbar = nil;
+ _textViewClass = nil;
+
+ [_typingIndicatorProxyView removeObserver:self forKeyPath:@"visible"];
+ _typingIndicatorProxyView = nil;
+ _typingIndicatorViewClass = nil;
+
+ _registeredPrefixes = nil;
+ _singleTapGesture.delegate = nil;
+ _singleTapGesture = nil;
+ _verticalPanGesture.delegate = nil;
+ _verticalPanGesture = nil;
+ _scrollViewHC = nil;
+ _textInputbarHC = nil;
+ _typingIndicatorViewHC = nil;
+ _autoCompletionViewHC = nil;
+ _keyboardHC = nil;
+
+ [self slk_unregisterNotifications];
+}
+
+@end
http://git-wip-us.apache.org/repos/asf/usergrid/blob/7442c881/sdks/swift/Samples/ActivityFeed/Pods/SlackTextViewController/Source/SLKTypingIndicatorProtocol.h
----------------------------------------------------------------------
diff --git a/sdks/swift/Samples/ActivityFeed/Pods/SlackTextViewController/Source/SLKTypingIndicatorProtocol.h b/sdks/swift/Samples/ActivityFeed/Pods/SlackTextViewController/Source/SLKTypingIndicatorProtocol.h
new file mode 100644
index 0000000..7b2deae
--- /dev/null
+++ b/sdks/swift/Samples/ActivityFeed/Pods/SlackTextViewController/Source/SLKTypingIndicatorProtocol.h
@@ -0,0 +1,37 @@
+//
+// Copyright 2014 Slack Technologies, Inc.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+
+#import <Foundation/Foundation.h>
+
+/** Generic protocol needed when customizing your own typing indicator view. */
+@protocol SLKTypingIndicatorProtocol <NSObject>
+@required
+
+/**
+ Returns YES if the indicator is visible.
+ SLKTextViewController depends on this property internally, by observing its value changes to update the typing indicator view's constraints automatically.
+ You can simply @synthesize this property to make it KVO compliant, or override its setter method and wrap its implementation with -willChangeValueForKey: and -didChangeValueForKey: methods, for more complex KVO compliance.
+ */
+@property (nonatomic, getter = isVisible) BOOL visible;
+
+@optional
+
+/**
+ Dismisses the indicator view.
+ */
+- (void)dismissIndicator;
+
+@end
\ No newline at end of file