blob: dd36ff1512c072f330a2a98c811e67d89536da82 [file] [log] [blame]
// Copyright 2018 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#import "ios/chrome/browser/ui/util/keyboard_observer_helper.h"
#include "base/check.h"
#include "ios/chrome/browser/ui/util/ui_util.h"
#import "ios/chrome/common/ui/util/constraints_ui_util.h"
#if !defined(__has_feature) || !__has_feature(objc_arc)
#error "This file requires ARC support."
#endif
// TODO(crbug.com/974226): look into making this a singleton with multiple
// listeners.
@interface KeyboardObserverHelper ()
// Flag that indicates if the keyboard is on screen.
@property(nonatomic, readwrite, getter=isKeyboardOnScreen)
BOOL keyboardOnScreen;
// Current keyboard state.
@property(nonatomic, readwrite, getter=getKeyboardState)
KeyboardState keyboardState;
// The last known keyboard view. If this changes, it probably means that the
// application lost focus in multiwindow mode.
@property(nonatomic, weak) UIView* keyboardView;
@end
@implementation KeyboardObserverHelper
#pragma mark - Public
- (instancetype)init {
self = [super init];
if (self) {
[[NSNotificationCenter defaultCenter]
addObserver:self
selector:@selector(keyboardWillShow:)
name:UIKeyboardWillShowNotification
object:nil];
[[NSNotificationCenter defaultCenter]
addObserver:self
selector:@selector(keyboardWillHide:)
name:UIKeyboardWillHideNotification
object:nil];
[[NSNotificationCenter defaultCenter]
addObserver:self
selector:@selector(keyboardWillDidChangeFrame:)
name:UIKeyboardWillChangeFrameNotification
object:nil];
[[NSNotificationCenter defaultCenter]
addObserver:self
selector:@selector(keyboardWillDidChangeFrame:)
name:UIKeyboardDidChangeFrameNotification
object:nil];
}
return self;
}
+ (UIView*)keyboardView {
NSArray* windows = [UIApplication sharedApplication].windows;
NSUInteger expectedMinWindows = IsIPadIdiom() ? 2 : 3;
if (windows.count < expectedMinWindows)
return nil;
UIWindow* window = windows.lastObject;
for (UIView* subview in window.subviews) {
if ([NSStringFromClass([subview class]) rangeOfString:@"PeripheralHost"]
.location != NSNotFound) {
return subview;
}
if ([NSStringFromClass([subview class]) rangeOfString:@"SetContainer"]
.location != NSNotFound) {
for (UIView* subsubview in subview.subviews) {
if ([NSStringFromClass([subsubview class]) rangeOfString:@"SetHost"]
.location != NSNotFound) {
return subsubview;
}
}
}
}
return nil;
}
+ (id<EdgeLayoutGuideProvider>)keyboardLayoutGuide {
// [iOS 13] Sometimes there is an input assistant provided by the system.
// I.e. autocorrection or passwords. A composed guide is needed because
// there isn't a view "hugging" the accessory and the keyboard. This used to
// be merged with the keyboard < iOS 13.
//
// [iOS 13][iOS 12][iPhone][iPhoneX]: "Backdrop" works even when a picker is
// present.
for (NSString* name in @[ @"InputAssistant", @"Backdrop" ]) {
id<EdgeLayoutGuideProvider> layout =
[self keyboardLayoutGuideInHostView:self.keyboardView withName:name];
if (layout) {
return layout;
}
}
return nil;
}
#pragma mark - Keyboard Notifications
- (void)keyboardWillDidChangeFrame:(NSNotification*)notification {
[self updateKeyboardState];
}
- (void)keyboardWillShow:(NSNotification*)notification {
self.keyboardOnScreen = YES;
}
- (void)keyboardWillHide:(NSNotification*)notification {
self.keyboardOnScreen = NO;
#if !defined(__IPHONE_13_0) || __IPHONE_OS_VERSION_MIN_REQUIRED < __IPHONE_13_0
dispatch_async(dispatch_get_main_queue(), ^{
if (self.keyboardOnScreen) {
[self.consumer keyboardDidStayOnScreen];
}
});
#endif
}
#pragma mark - Private
// Returns a layout guide defined by the top edge of |view| and its window.
+ (id<EdgeLayoutGuideProvider>)topEdgeLayoutGuideForView:(UIView*)view {
ComposedEdgeLayoutGuide* layoutGuide = [[ComposedEdgeLayoutGuide alloc] init];
layoutGuide.baseLayoutGuide = view.window;
layoutGuide.topAnchorProvider = view;
return layoutGuide;
}
// This searches in the passed view hierarchy for the best Layout Guide for the
// keyboard. Unexpected behaviour on iPad.
+ (id<EdgeLayoutGuideProvider>)keyboardLayoutGuideInHostView:(UIView*)hostView
withName:(NSString*)name {
DCHECK(!IsIPadIdiom());
for (UIView* subview in hostView.subviews) {
if ([NSStringFromClass([subview class]) containsString:name]) {
return [self topEdgeLayoutGuideForView:subview];
}
// Continue searching recursively.
id<EdgeLayoutGuideProvider> found =
[self keyboardLayoutGuideInHostView:subview withName:name];
if (found) {
return found;
}
}
return nil;
}
#pragma mark Keyboard State Detection
// Update keyboard state by looking at keyboard frame and the existence of some
// classes to detect split view or pickers.
- (void)updateKeyboardState {
UIView* keyboardView = KeyboardObserverHelper.keyboardView;
CGFloat windowHeight = [UIScreen mainScreen].bounds.size.height;
CGRect keyboardFrame = keyboardView.frame;
BOOL isVisible = CGRectGetMinY(keyboardFrame) < windowHeight;
BOOL isUndocked = CGRectGetMaxY(keyboardFrame) < windowHeight;
BOOL isHardware = isVisible && CGRectGetMaxY(keyboardFrame) > windowHeight;
BOOL isSplit = [self viewIsSplit:keyboardView];
BOOL isPicker = [self containsPickerView:keyboardView];
// Only notify if a change is detected.
if (isVisible != self.keyboardState.isVisible ||
isUndocked != self.keyboardState.isUndocked ||
isSplit != self.keyboardState.isSplit ||
isHardware != self.keyboardState.isHardware ||
isPicker != self.keyboardState.isPicker ||
keyboardView != self.keyboardView) {
self.keyboardState = {isVisible, isUndocked, isSplit, isHardware, isPicker};
self.keyboardView = keyboardView;
dispatch_async(dispatch_get_main_queue(), ^{
[self.consumer keyboardWillChangeToState:self.keyboardState];
});
}
}
// Checks for a picker UIView* under the given |view|.
- (BOOL)containsPickerView:(UIView*)view {
for (UIView* subview in view.subviews) {
if ([NSStringFromClass([subview class]) rangeOfString:@"Picker"].location !=
NSNotFound) {
return YES;
}
}
return NO;
}
// Checks for the presence of split image views under the given |view|.
- (BOOL)viewIsSplit:(UIView*)view {
// Don't waste time going through the accessory views.
if ([NSStringFromClass([view class]) rangeOfString:@"FormInputAccessoryView"]
.location != NSNotFound) {
return NO;
}
for (UIView* subview in view.subviews) {
if ([NSStringFromClass([subview class]) rangeOfString:@"SplitImage"]
.location != NSNotFound) {
return subview.subviews.count > 1;
}
if ([self viewIsSplit:subview])
return YES;
}
return NO;
}
@end