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
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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