diff --git a/Examples/Messenger-Storyboard/Base.lproj/Main.storyboard b/Examples/Messenger-Storyboard/Base.lproj/Main.storyboard index 29b45d46..43ba7a05 100644 --- a/Examples/Messenger-Storyboard/Base.lproj/Main.storyboard +++ b/Examples/Messenger-Storyboard/Base.lproj/Main.storyboard @@ -1,14 +1,14 @@ - + - + - + diff --git a/Source/SLKInputAccessoryView.m b/Source/SLKInputAccessoryView.m index 0e1727b9..97e7ae87 100644 --- a/Source/SLKInputAccessoryView.m +++ b/Source/SLKInputAccessoryView.m @@ -16,9 +16,7 @@ @implementation SLKInputAccessoryView - (void)willMoveToSuperview:(UIView *)newSuperview { - if (!SLK_IS_IOS9_AND_HIGHER) { - _keyboardViewProxy = newSuperview; - } + _keyboardViewProxy = newSuperview; } @end \ No newline at end of file diff --git a/Source/SLKTextViewController.h b/Source/SLKTextViewController.h index 55504c1f..0a37522b 100644 --- a/Source/SLKTextViewController.h +++ b/Source/SLKTextViewController.h @@ -74,21 +74,19 @@ NS_CLASS_AVAILABLE_IOS(7_0) @interface SLKTextViewController : UIViewController /** A single tap gesture used to dismiss the keyboard. SLKTextViewController is its delegate. */ @property (nonatomic, readonly) UIGestureRecognizer *singleTapGesture; -/** A vertical pan gesture used for bringing the keyboard from the bottom. SLKTextViewController is its delegate. */ +/** A vertical pan gesture used for moving the keyboard up and bottom. SLKTextViewController is its delegate. */ @property (nonatomic, readonly) UIPanGestureRecognizer *verticalPanGesture; +/** A vertical swipe gesture used for bringing the keyboard from the bottom. SLKTextViewController is its delegate. */ +@property (nonatomic, readonly) UISwipeGestureRecognizer *verticalSwipeGesture; + /** YES if animations should have bouncy effects. Default is YES. */ @property (nonatomic, assign) BOOL bounces; /** YES if text view's content can be cleaned with a shake gesture. Default is NO. */ @property (nonatomic, assign) BOOL shakeToClearEnabled; -/** - YES if keyboard can be dismissed gradually with a vertical panning gesture. Default is YES. - - This feature doesn't work on iOS 9 due to no legit alternatives to detect the keyboard view. - Open Radar: http://openradar.appspot.com/radar?id=5021485877952512 - */ +/** YES if keyboard can be dismissed gradually with a vertical panning gesture. Default is YES. */ @property (nonatomic, assign, getter = isKeyboardPanningEnabled) BOOL keyboardPanningEnabled; /** YES if an external keyboard has been detected (this value updates only when the text view becomes first responder). */ @@ -565,6 +563,7 @@ NS_CLASS_AVAILABLE_IOS(7_0) @interface SLKTextViewController : UIViewController /** UIGestureRecognizerDelegate */ - (BOOL)gestureRecognizerShouldBegin:(UIGestureRecognizer *)gestureRecognizer NS_REQUIRES_SUPER; +- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldRecognizeSimultaneouslyWithGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer NS_REQUIRES_SUPER; /** UIAlertViewDelegate */ #ifndef __IPHONE_8_0 diff --git a/Source/SLKTextViewController.m b/Source/SLKTextViewController.m index 7d8ed25a..c34a0dec 100644 --- a/Source/SLKTextViewController.m +++ b/Source/SLKTextViewController.m @@ -12,9 +12,6 @@ #import "UIResponder+SLKAdditions.h" #import "SLKUIConstants.h" -/** Feature flagged while waiting to implement a more reliable technique. */ -#define SLKBottomPanningEnabled 0 - #define kSLKAlertViewClearTextTag [NSStringFromClass([SLKTextViewController class]) hash] NSString * const SLKKeyboardWillShowNotification = @"SLKKeyboardWillShowNotification"; @@ -36,6 +33,9 @@ @interface SLKTextViewController () // A hairline displayed on top of the auto-completion view, to better separate the content from the control. @property (nonatomic, strong) UIView *autoCompletionHairline; +// A view to stub the keyboard during the keyboard panning transition. +@property (nonatomic, strong) UIView *keyboardStubView; + // Auto-Layout height constraints used for updating their constants @property (nonatomic, strong) NSLayoutConstraint *scrollViewHC; @property (nonatomic, strong) NSLayoutConstraint *textInputbarHC; @@ -50,7 +50,7 @@ @interface SLKTextViewController () @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; +@property (nonatomic, getter=isTransitioning) BOOL transitioning; // Optional classes to be used instead of the default ones. @property (nonatomic, strong) Class textViewClass; @@ -318,8 +318,12 @@ - (SLKTextInputbar *)textInputbar _verticalPanGesture = [[UIPanGestureRecognizer alloc] initWithTarget:self action:@selector(slk_didPanTextInputBar:)]; _verticalPanGesture.delegate = self; - [_textInputbar addGestureRecognizer:self.verticalPanGesture]; + + _verticalSwipeGesture = [[UISwipeGestureRecognizer alloc] initWithTarget:self action:@selector(slk_didSwipeTextInputBar:)]; + _verticalSwipeGesture.direction = UISwipeGestureRecognizerDirectionUp; + _verticalSwipeGesture.delegate = self; + [_textInputbar addGestureRecognizer:self.verticalSwipeGesture]; } return _textInputbar; } @@ -511,6 +515,15 @@ - (BOOL)slk_isIllogicalKeyboardStatus:(SLKKeyboardStatus)newStatus return YES; } +- (UIWindow *)slk_keyboardWindow +{ + NSArray *array = [[UIApplication sharedApplication] windows]; + + // NOTE: This is risky, since the order may change in the future + // but it is the only way of looking up for the keyboard's window without using private APIs. + return [array lastObject]; +} + #pragma mark - Setters @@ -894,100 +907,55 @@ - (void)setTextInputbarHidden:(BOOL)hidden animated:(BOOL)animated #pragma mark - Private Methods -- (void)slk_didPanTextInputBar:(UIPanGestureRecognizer *)gesture +- (void)slk_didSwipeTextInputBar:(UISwipeGestureRecognizer *)gesture { - // Textinput dragging isn't supported when - if (!self.view.window || !self.keyboardPanningEnabled || - [self ignoreTextInputbarAdjustment] || self.isPresentedInPopover) { - return; + if (gesture.state == UIGestureRecognizerStateEnded) { + if (!self.isPresentedInPopover && ![self ignoreTextInputbarAdjustment]) { + [self presentKeyboard:YES]; + } } - - dispatch_async(dispatch_get_main_queue(), ^{ - [self slk_handlePanGestureRecognizer:gesture]; - }); } -- (void)slk_handlePanGestureRecognizer:(UIPanGestureRecognizer *)gesture +- (void)slk_didPanTextInputBar:(UIPanGestureRecognizer *)gesture { - // Local variables static CGPoint startPoint; static CGRect originalFrame; static BOOL dragging = NO; - static BOOL presenting = NO; - __block UIView *keyboardView = [_textInputbar.inputAccessoryView keyboardViewProxy]; + __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. - 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; - } + [self slk_prepareKeyboardStub]; break; } case UIGestureRecognizerStateChanged: { - if (CGRectContainsPoint(_textInputbar.frame, gestureLocation) || dragging || presenting){ - + if (CGRectContainsPoint(self.textInputbar.frame, gestureLocation) || dragging){ if (CGPointEqualToPoint(startPoint, CGPointZero)) { startPoint = gestureLocation; dragging = YES; - if (!presenting) { - originalFrame = keyboardView.frame; - } + // Displays the keyboard stub in the key windows's hierarchy. + [self slk_showKeyboardStub:YES]; + + originalFrame = keyboardView.frame; } self.movingKeyboard = YES; @@ -995,21 +963,14 @@ - (void)slk_handlePanGestureRecognizer:(UIPanGestureRecognizer *)gesture 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); - } + 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.keyboardStubView.frame = [self.view.window convertRect:keyboardFrame fromView:nil]; self.keyboardHC.constant = [self slk_appropriateKeyboardHeightFromRect:keyboardFrame]; self.scrollViewHC.constant = [self slk_appropriateScrollViewHeight]; @@ -1041,24 +1002,22 @@ - (void)slk_handlePanGestureRecognizer:(UIPanGestureRecognizer *)gesture case UIGestureRecognizerStateFailed: { if (!dragging) { + [self slk_showKeyboardStub:NO]; 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); + BOOL hide = (gestureVelocity.y > minVelocity) || (transition.y > minDistance); - if (hide) keyboardFrame.origin.y = keyboardMaxY; + if (hide) { + keyboardFrame.origin.y = keyboardMaxY; + } self.keyboardHC.constant = [self slk_appropriateKeyboardHeightFromRect:keyboardFrame]; self.scrollViewHC.constant = [self slk_appropriateScrollViewHeight]; @@ -1068,7 +1027,7 @@ - (void)slk_handlePanGestureRecognizer:(UIPanGestureRecognizer *)gesture options:UIViewAnimationOptionCurveEaseInOut|UIViewAnimationOptionBeginFromCurrentState animations:^{ [self.view layoutIfNeeded]; - keyboardView.frame = keyboardFrame; + self.keyboardStubView.frame = [self.view.window convertRect:keyboardFrame fromView:nil]; } completion:^(BOOL finished) { if (hide) { @@ -1079,9 +1038,10 @@ - (void)slk_handlePanGestureRecognizer:(UIPanGestureRecognizer *)gesture startPoint = CGPointZero; originalFrame = CGRectZero; dragging = NO; - presenting = NO; self.movingKeyboard = NO; + + [self slk_showKeyboardStub:NO]; }]; break; @@ -1099,11 +1059,6 @@ - (void)slk_didTapScrollView:(UIGestureRecognizer *)gesture } } -- (void)slk_didPanTextView:(UIGestureRecognizer *)gesture -{ - [self presentKeyboard:YES]; -} - - (void)slk_performRightAction { NSArray *actions = [self.rightButton actionsForTarget:self forControlEvent:UIControlEventTouchUpInside]; @@ -1275,6 +1230,60 @@ - (void)slk_prepareForInterfaceTransitionWithDuration:(NSTimeInterval)duration }); } +// Takes a snapshot of the keyboard window and caches it in a wrapper view +// to be used later as a keyboard stub and give the illusion that the keyboard is being panned by the user. +- (void)slk_prepareKeyboardStub +{ + UIWindow *keyboardWindow = [self slk_keyboardWindow]; + + if (!_keyboardStubView && keyboardWindow) { + // Takes a snapshot of the keyboard's window + UIView *snapshotView = [keyboardWindow snapshotViewAfterScreenUpdates:NO]; + UIView *keyboardView = [self.textInputbar.inputAccessoryView keyboardViewProxy]; + + CGRect screenBounds = [UIScreen mainScreen].bounds; + + // Shifts the snapshot up to fit to the bottom + CGRect snapshowFrame = snapshotView.frame; + snapshowFrame.origin.y = CGRectGetHeight(keyboardView.frame) - CGRectGetHeight(screenBounds); + snapshotView.frame = snapshowFrame; + + self.keyboardStubView = [[UIView alloc] init]; + self.keyboardStubView.backgroundColor = [UIColor clearColor]; + [self.keyboardStubView addSubview:snapshotView]; + } +} + +// Shows/Hides the keyboard stub +- (void)slk_showKeyboardStub:(BOOL)show +{ + UIWindow *keyboardWindow = [self slk_keyboardWindow]; + int64_t delay = NSEC_PER_SEC * 0.025; + + if (!keyboardWindow || !self.keyboardStubView) { + return; + } + + if (show) { + // Adds the stub view to the key window, to overlap any other view in the hierarchy + [self.view.window addSubview:self.keyboardStubView]; + + // Let's delay hiding the keyboard's window to avoid noticeable glitches + dispatch_after(dispatch_time(DISPATCH_TIME_NOW, delay), dispatch_get_main_queue(), ^{ + keyboardWindow.hidden = YES; + }); + } + else { + keyboardWindow.hidden = NO; + + // Let's the removal of the keyboard stub to avoid noticeable glitches + dispatch_after(dispatch_time(DISPATCH_TIME_NOW, delay), dispatch_get_main_queue(), ^{ + [_keyboardStubView removeFromSuperview]; + _keyboardStubView = nil; + }); + } +} + #pragma mark - Keyboard Events @@ -2102,10 +2111,38 @@ - (void)scrollViewDidScroll:(UIScrollView *)scrollView - (BOOL)gestureRecognizerShouldBegin:(UIGestureRecognizer *)gesture { if ([gesture isEqual:self.singleTapGesture]) { - return [self.textView isFirstResponder] && ![self ignoreTextInputbarAdjustment]; + // Tap to dismiss isn't supported when + if (![self.textView isFirstResponder] && [self ignoreTextInputbarAdjustment]) { + return NO; + } + + return YES; + } + else if ([gesture isEqual:self.verticalSwipeGesture]) { + // TextInput swipping isn't supported when + if (!self.view.window || [self.textView isFirstResponder] || [self ignoreTextInputbarAdjustment] || self.isPresentedInPopover) { + return NO; + } + + return YES; } else if ([gesture isEqual:self.verticalPanGesture]) { - return self.keyboardPanningEnabled && ![self ignoreTextInputbarAdjustment]; + // TextInput dragging isn't supported when + if (!self.view.window || !self.keyboardPanningEnabled || [self ignoreTextInputbarAdjustment] || self.isPresentedInPopover) { + return NO; + } + + return YES; + } + + return NO; +} + +- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldRecognizeSimultaneouslyWithGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer +{ + if ([otherGestureRecognizer isKindOfClass:[UISwipeGestureRecognizer class]] && + [gestureRecognizer isEqual:self.verticalPanGesture] && [otherGestureRecognizer isEqual:self.verticalSwipeGesture]) { + return YES; } return NO;