Skip to content
This repository was archived by the owner on Oct 30, 2018. It is now read-only.

Implements an alternative technique for dragging the keyboard on iOS 9 #355

Closed
wants to merge 11 commits into from
5 changes: 1 addition & 4 deletions Source/SLKInputAccessoryView.m
Original file line number Diff line number Diff line change
Expand Up @@ -20,14 +20,11 @@

@implementation SLKInputAccessoryView


#pragma mark - Super Overrides

- (void)willMoveToSuperview:(UIView *)newSuperview
{
if (!SLK_IS_IOS9_AND_HIGHER) {
_keyboardViewProxy = newSuperview;
}
_keyboardViewProxy = newSuperview;
}

@end
13 changes: 13 additions & 0 deletions Source/SLKTextInputbar.h
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,19 @@ typedef NS_ENUM(NSUInteger, SLKCounterPosition) {
*/
- (void)endTextEdition;

/**
Shows a keyboard snapshot placeholder, replacing the system keyboard.
The snapshot is being added as a subview, aligned at the same position the keyboard is, before hiding it momentarily.

@param view The view where the snapshot is taken for, for aligning purposes.
*/
- (void)prepareKeyboardPlaceholderFromView:(UIView *)view;

/**
Hides the visible keyboard snapshot placeholder, if applicable.
*/
- (void)showKeyboardPlaceholder:(BOOL)show;


#pragma mark - Text Counting
///------------------------------------------------
Expand Down
60 changes: 60 additions & 0 deletions Source/SLKTextInputbar.m
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ @interface SLKTextInputbar ()
@property (nonatomic, strong) NSArray *charCountLabelVCs;

@property (nonatomic, strong) UILabel *charCountLabel;
@property (nonatomic, strong) UIView *keyboardPlaceholderView;

@property (nonatomic) CGPoint previousOrigin;

Expand Down Expand Up @@ -538,6 +539,65 @@ - (void)slk_updateCounter
}


#pragma mark - Keyboard Snapshot Placeholder

- (void)prepareKeyboardPlaceholderFromView:(UIView *)view
{
UIWindow *keyboardWindow = [self slk_keyboardWindow];

if (!_keyboardPlaceholderView && keyboardWindow) {
// Takes a snapshot of the keyboard's window
UIView *snapshotView = [keyboardWindow snapshotViewAfterScreenUpdates:NO];

CGRect screenBounds = [UIScreen mainScreen].bounds;

// Shifts the snapshot up to fit to the bottom
CGRect snapshowFrame = snapshotView.frame;
snapshowFrame.origin.y = CGRectGetHeight(self.inputAccessoryView.keyboardViewProxy.frame) - CGRectGetHeight(screenBounds);
snapshotView.frame = snapshowFrame;

CGRect keyboardFrame = self.inputAccessoryView.keyboardViewProxy.frame;
keyboardFrame.origin.y = CGRectGetHeight(self.frame);

self.keyboardPlaceholderView = [[UIView alloc] initWithFrame:keyboardFrame];
self.keyboardPlaceholderView.backgroundColor = [UIColor clearColor];
[self.keyboardPlaceholderView addSubview:snapshotView];
}
}

- (void)showKeyboardPlaceholder:(BOOL)show
{
UIWindow *keyboardWindow = [self slk_keyboardWindow];

if (show && self.keyboardPlaceholderView && keyboardWindow) {

// Adds the placeholder view to the input bar, so when it looks they are sticked together.
[self addSubview:self.keyboardPlaceholderView];

// Let's delay hiding the keyboard's window to avoid noticeable glitches
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.01 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
keyboardWindow.hidden = YES;
});
}
else if (!show && _keyboardPlaceholderView && keyboardWindow) {

[_keyboardPlaceholderView removeFromSuperview];
_keyboardPlaceholderView = nil;

keyboardWindow.hidden = NO;
}
}

- (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 window without using private APIs.
return [array lastObject];
}


#pragma mark - Notification Events

- (void)slk_didChangeTextViewText:(NSNotification *)notification
Expand Down
9 changes: 5 additions & 4 deletions Source/SLKTextViewController.h
Original file line number Diff line number Diff line change
Expand Up @@ -80,9 +80,12 @@ 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;

Expand All @@ -91,9 +94,6 @@ NS_CLASS_AVAILABLE_IOS(7_0) @interface SLKTextViewController : UIViewController

/**
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
*/
@property (nonatomic, assign, getter = isKeyboardPanningEnabled) BOOL keyboardPanningEnabled;

Expand Down Expand Up @@ -558,6 +558,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
Expand Down
142 changes: 64 additions & 78 deletions Source/SLKTextViewController.m
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,6 @@

#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";
Expand Down Expand Up @@ -323,8 +320,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;
}
Expand Down Expand Up @@ -909,26 +910,20 @@ - (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];

Expand All @@ -944,78 +939,44 @@ - (void)slk_handlePanGestureRecognizer:(UIPanGestureRecognizer *)gesture
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:_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;
// Because the keyboard is on its own view hierarchy since iOS 9,
// we instead show a snapshot of the keyboard and hide it
// to give the illusion that the keyboard is being moved by the user.
if (SLK_IS_IOS9_AND_HIGHER && gestureVelocity.y > 0) {
[self.textInputbar prepareKeyboardPlaceholderFromView:self.view];
}

break;
}
case UIGestureRecognizerStateChanged: {

if (CGRectContainsPoint(_textInputbar.frame, gestureLocation) || dragging || presenting){

if (CGRectContainsPoint(_textInputbar.frame, gestureLocation) || dragging){
if (CGPointEqualToPoint(startPoint, CGPointZero)) {
startPoint = gestureLocation;
dragging = YES;

if (!presenting) {
originalFrame = keyboardView.frame;
// Because the keyboard is on its own view hierarchy since iOS 9,
// we instead show a snapshot of the keyboard and hide it
// to give the illusion that the keyboard is being moved by the user.
if (SLK_IS_IOS9_AND_HIGHER && gestureVelocity.y > 0) {
[self.textInputbar showKeyboardPlaceholder:YES];
}

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);
}
keyboardFrame.origin.y += MAX(transition.y, 0.0);

// Makes sure they keyboard is always anchored to the bottom
if (CGRectGetMinY(keyboardFrame) < keyboardMinY) {
Expand Down Expand Up @@ -1055,22 +1016,21 @@ - (void)slk_handlePanGestureRecognizer:(UIPanGestureRecognizer *)gesture
case UIGestureRecognizerStateFailed: {

if (!dragging) {
if (SLK_IS_IOS9_AND_HIGHER) {
[self.textInputbar showKeyboardPlaceholder: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;

Expand All @@ -1093,9 +1053,12 @@ - (void)slk_handlePanGestureRecognizer:(UIPanGestureRecognizer *)gesture
startPoint = CGPointZero;
originalFrame = CGRectZero;
dragging = NO;
presenting = NO;

self.movingKeyboard = NO;

if (SLK_IS_IOS9_AND_HIGHER) {
[self.textInputbar showKeyboardPlaceholder:NO];
}
}];

break;
Expand All @@ -1113,11 +1076,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];
Expand Down Expand Up @@ -2135,10 +2093,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;
Expand Down