blob: 0b20fbdce9232b6e1ffa6b9e78bef11c9feeebc8 [file] [log] [blame]
//
// Created by Jesse Squires
// http://www.jessesquires.com
//
//
// Documentation
// http://cocoadocs.org/docsets/JSQMessagesViewController
//
//
// GitHub
// https://github.com/jessesquires/JSQMessagesViewController
//
//
// License
// Copyright (c) 2014 Jesse Squires
// Released under an MIT license: http://opensource.org/licenses/MIT
//
//
// Ideas for keyboard controller taken from Daniel Amitay
// DAKeyboardControl
// https://github.com/danielamitay/DAKeyboardControl
//
#import "JSQMessagesKeyboardController.h"
#import "UIDevice+JSQMessages.h"
NSString * const JSQMessagesKeyboardControllerNotificationKeyboardDidChangeFrame = @"JSQMessagesKeyboardControllerNotificationKeyboardDidChangeFrame";
NSString * const JSQMessagesKeyboardControllerUserInfoKeyKeyboardDidChangeFrame = @"JSQMessagesKeyboardControllerUserInfoKeyKeyboardDidChangeFrame";
static void * kJSQMessagesKeyboardControllerKeyValueObservingContext = &kJSQMessagesKeyboardControllerKeyValueObservingContext;
typedef void (^JSQAnimationCompletionBlock)(BOOL finished);
@interface JSQMessagesKeyboardController () <UIGestureRecognizerDelegate>
@property (assign, nonatomic) BOOL jsq_isObserving;
@property (strong, nonatomic) UIView *keyboardView;
@end
@implementation JSQMessagesKeyboardController
#pragma mark - Initialization
- (instancetype)initWithTextView:(UITextView *)textView
contextView:(UIView *)contextView
panGestureRecognizer:(UIPanGestureRecognizer *)panGestureRecognizer
delegate:(id<JSQMessagesKeyboardControllerDelegate>)delegate
{
NSParameterAssert(textView != nil);
NSParameterAssert(contextView != nil);
NSParameterAssert(panGestureRecognizer != nil);
self = [super init];
if (self) {
_textView = textView;
_contextView = contextView;
_panGestureRecognizer = panGestureRecognizer;
_delegate = delegate;
_jsq_isObserving = NO;
}
return self;
}
- (void)dealloc
{
[self jsq_removeKeyboardFrameObserver];
[self jsq_unregisterForNotifications];
_panGestureRecognizer = nil;
_delegate = nil;
}
#pragma mark - Setters
- (void)setKeyboardView:(UIView *)keyboardView
{
if (_keyboardView) {
[self jsq_removeKeyboardFrameObserver];
}
_keyboardView = keyboardView;
if (keyboardView && !_jsq_isObserving) {
[_keyboardView addObserver:self
forKeyPath:NSStringFromSelector(@selector(frame))
options:(NSKeyValueObservingOptionOld | NSKeyValueObservingOptionNew)
context:kJSQMessagesKeyboardControllerKeyValueObservingContext];
_jsq_isObserving = YES;
}
}
#pragma mark - Getters
- (BOOL)keyboardIsVisible
{
return self.keyboardView != nil;
}
- (CGRect)currentKeyboardFrame
{
if (!self.keyboardIsVisible) {
return CGRectNull;
}
return self.keyboardView.frame;
}
#pragma mark - Keyboard controller
- (void)beginListeningForKeyboard
{
if (self.textView.inputAccessoryView == nil) {
self.textView.inputAccessoryView = [[UIView alloc] init];
}
[self jsq_registerForNotifications];
}
- (void)endListeningForKeyboard
{
[self jsq_unregisterForNotifications];
[self jsq_setKeyboardViewHidden:NO];
self.keyboardView = nil;
}
#pragma mark - Notifications
- (void)jsq_registerForNotifications
{
[self jsq_unregisterForNotifications];
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(jsq_didReceiveKeyboardDidShowNotification:)
name:UIKeyboardDidShowNotification
object:nil];
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(jsq_didReceiveKeyboardWillChangeFrameNotification:)
name:UIKeyboardWillChangeFrameNotification
object:nil];
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(jsq_didReceiveKeyboardDidChangeFrameNotification:)
name:UIKeyboardDidChangeFrameNotification
object:nil];
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(jsq_didReceiveKeyboardDidHideNotification:)
name:UIKeyboardDidHideNotification
object:nil];
}
- (void)jsq_unregisterForNotifications
{
[[NSNotificationCenter defaultCenter] removeObserver:self];
}
- (void)jsq_didReceiveKeyboardDidShowNotification:(NSNotification *)notification
{
UIView *keyboardViewProxy = self.textView.inputAccessoryView.superview;
if ([UIDevice jsq_isCurrentDeviceAfteriOS9]) {
NSPredicate *windowPredicate = [NSPredicate predicateWithFormat:@"self isMemberOfClass: %@", NSClassFromString(@"UIRemoteKeyboardWindow")];
UIWindow *keyboardWindow = [[UIApplication sharedApplication].windows filteredArrayUsingPredicate:windowPredicate].firstObject;
for (UIView *subview in keyboardWindow.subviews) {
for (UIView *hostview in subview.subviews) {
if ([hostview isMemberOfClass:NSClassFromString(@"UIInputSetHostView")]) {
keyboardViewProxy = hostview;
break;
}
}
}
self.keyboardView = keyboardViewProxy;
}
[self jsq_setKeyboardViewHidden:NO];
[self jsq_handleKeyboardNotification:notification completion:^(BOOL finished) {
[self.panGestureRecognizer addTarget:self action:@selector(jsq_handlePanGestureRecognizer:)];
}];
}
- (void)jsq_didReceiveKeyboardWillChangeFrameNotification:(NSNotification *)notification
{
[self jsq_handleKeyboardNotification:notification completion:nil];
}
- (void)jsq_didReceiveKeyboardDidChangeFrameNotification:(NSNotification *)notification
{
[self jsq_setKeyboardViewHidden:NO];
[self jsq_handleKeyboardNotification:notification completion:nil];
}
- (void)jsq_didReceiveKeyboardDidHideNotification:(NSNotification *)notification
{
self.keyboardView = nil;
[self jsq_handleKeyboardNotification:notification completion:^(BOOL finished) {
[self.panGestureRecognizer removeTarget:self action:NULL];
}];
}
- (void)jsq_handleKeyboardNotification:(NSNotification *)notification completion:(JSQAnimationCompletionBlock)completion
{
NSDictionary *userInfo = [notification userInfo];
CGRect keyboardEndFrame = [userInfo[UIKeyboardFrameEndUserInfoKey] CGRectValue];
if (CGRectIsNull(keyboardEndFrame)) {
return;
}
UIViewAnimationCurve animationCurve = [userInfo[UIKeyboardAnimationCurveUserInfoKey] integerValue];
NSInteger animationCurveOption = (animationCurve << 16);
double animationDuration = [userInfo[UIKeyboardAnimationDurationUserInfoKey] doubleValue];
CGRect keyboardEndFrameConverted = [self.contextView convertRect:keyboardEndFrame fromView:nil];
[UIView animateWithDuration:animationDuration
delay:0.0
options:animationCurveOption
animations:^{
[self jsq_notifyKeyboardFrameNotificationForFrame:keyboardEndFrameConverted];
}
completion:^(BOOL finished) {
if (completion) {
completion(finished);
}
}];
}
#pragma mark - Utilities
- (void)jsq_setKeyboardViewHidden:(BOOL)hidden
{
self.keyboardView.hidden = hidden;
self.keyboardView.userInteractionEnabled = !hidden;
}
- (void)jsq_notifyKeyboardFrameNotificationForFrame:(CGRect)frame
{
[self.delegate keyboardController:self keyboardDidChangeFrame:frame];
[[NSNotificationCenter defaultCenter] postNotificationName:JSQMessagesKeyboardControllerNotificationKeyboardDidChangeFrame
object:self
userInfo:@{ JSQMessagesKeyboardControllerUserInfoKeyKeyboardDidChangeFrame : [NSValue valueWithCGRect:frame] }];
}
- (void)jsq_resetKeyboardAndTextView
{
[self jsq_setKeyboardViewHidden:YES];
[self jsq_removeKeyboardFrameObserver];
[self.textView resignFirstResponder];
}
#pragma mark - Key-value observing
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
{
if (context == kJSQMessagesKeyboardControllerKeyValueObservingContext) {
if (object == self.keyboardView && [keyPath isEqualToString:NSStringFromSelector(@selector(frame))]) {
CGRect oldKeyboardFrame = [[change objectForKey:NSKeyValueChangeOldKey] CGRectValue];
CGRect newKeyboardFrame = [[change objectForKey:NSKeyValueChangeNewKey] CGRectValue];
if (CGRectEqualToRect(newKeyboardFrame, oldKeyboardFrame) || CGRectIsNull(newKeyboardFrame)) {
return;
}
CGRect keyboardEndFrameConverted = [self.contextView convertRect:newKeyboardFrame
fromView:self.keyboardView.superview];
[self jsq_notifyKeyboardFrameNotificationForFrame:keyboardEndFrameConverted];
}
}
}
- (void)jsq_removeKeyboardFrameObserver
{
if (!_jsq_isObserving) {
return;
}
@try {
[_keyboardView removeObserver:self
forKeyPath:NSStringFromSelector(@selector(frame))
context:kJSQMessagesKeyboardControllerKeyValueObservingContext];
}
@catch (NSException * __unused exception) { }
_jsq_isObserving = NO;
}
#pragma mark - Pan gesture recognizer
- (void)jsq_handlePanGestureRecognizer:(UIPanGestureRecognizer *)pan
{
CGPoint touch = [pan locationInView:self.contextView.window];
// system keyboard is added to a new UIWindow, need to operate in window coordinates
// also, keyboard always slides from bottom of screen, not the bottom of a view
CGFloat contextViewWindowHeight = CGRectGetHeight(self.contextView.window.frame);
if ([UIDevice jsq_isCurrentDeviceBeforeiOS8]) {
// handle iOS 7 bug when rotating to landscape
if (UIInterfaceOrientationIsLandscape([UIApplication sharedApplication].statusBarOrientation)) {
contextViewWindowHeight = CGRectGetWidth(self.contextView.window.frame);
}
}
CGFloat keyboardViewHeight = CGRectGetHeight(self.keyboardView.frame);
CGFloat dragThresholdY = (contextViewWindowHeight - keyboardViewHeight - self.keyboardTriggerPoint.y);
CGRect newKeyboardViewFrame = self.keyboardView.frame;
BOOL userIsDraggingNearThresholdForDismissing = (touch.y > dragThresholdY);
self.keyboardView.userInteractionEnabled = !userIsDraggingNearThresholdForDismissing;
switch (pan.state) {
case UIGestureRecognizerStateChanged:
{
newKeyboardViewFrame.origin.y = touch.y + self.keyboardTriggerPoint.y;
// bound frame between bottom of view and height of keyboard
newKeyboardViewFrame.origin.y = MIN(newKeyboardViewFrame.origin.y, contextViewWindowHeight);
newKeyboardViewFrame.origin.y = MAX(newKeyboardViewFrame.origin.y, contextViewWindowHeight - keyboardViewHeight);
if (CGRectGetMinY(newKeyboardViewFrame) == CGRectGetMinY(self.keyboardView.frame)) {
return;
}
[UIView animateWithDuration:0.0
delay:0.0
options:UIViewAnimationOptionBeginFromCurrentState | UIViewAnimationOptionTransitionNone
animations:^{
self.keyboardView.frame = newKeyboardViewFrame;
}
completion:nil];
}
break;
case UIGestureRecognizerStateEnded:
case UIGestureRecognizerStateCancelled:
case UIGestureRecognizerStateFailed:
{
BOOL keyboardViewIsHidden = (CGRectGetMinY(self.keyboardView.frame) >= contextViewWindowHeight);
if (keyboardViewIsHidden) {
[self jsq_resetKeyboardAndTextView];
return;
}
CGPoint velocity = [pan velocityInView:self.contextView];
BOOL userIsScrollingDown = (velocity.y > 0.0f);
BOOL shouldHide = (userIsScrollingDown && userIsDraggingNearThresholdForDismissing);
newKeyboardViewFrame.origin.y = shouldHide ? contextViewWindowHeight : (contextViewWindowHeight - keyboardViewHeight);
[UIView animateWithDuration:0.25
delay:0.0
options:UIViewAnimationOptionBeginFromCurrentState | UIViewAnimationCurveEaseOut
animations:^{
self.keyboardView.frame = newKeyboardViewFrame;
}
completion:^(BOOL finished) {
self.keyboardView.userInteractionEnabled = !shouldHide;
if (shouldHide) {
[self jsq_resetKeyboardAndTextView];
}
}];
}
break;
default:
break;
}
}
@end