Skip to content
This repository was archived by the owner on Feb 25, 2025. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from 7 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
241 changes: 204 additions & 37 deletions shell/platform/darwin/ios/framework/Source/FlutterViewController.mm
Original file line number Diff line number Diff line change
Expand Up @@ -315,6 +315,11 @@ - (void)setupNotificationCenterObservers {
name:UIKeyboardWillChangeFrameNotification
object:nil];

[center addObserver:self
selector:@selector(keyboardWillShowNotification:)
name:UIKeyboardWillShowNotification
object:nil];

[center addObserver:self
selector:@selector(keyboardWillBeHidden:)
name:UIKeyboardWillHideNotification
Expand Down Expand Up @@ -588,6 +593,13 @@ - (UIView*)keyboardAnimationView {
return _keyboardAnimationView.get();
}

- (UIScreen*)getMainScreen {
Comment thread
vashworth marked this conversation as resolved.
Outdated
if (@available(iOS 13.0, *)) {
return self.view.window.windowScene.screen;
}
return UIScreen.mainScreen;
}

- (BOOL)loadDefaultSplashScreenView {
NSString* launchscreenName =
[[[NSBundle mainBundle] infoDictionary] objectForKey:@"UILaunchStoryboardName"];
Expand Down Expand Up @@ -1272,65 +1284,220 @@ - (void)updateViewportPadding {

#pragma mark - Keyboard events

typedef NS_ENUM(NSInteger, FlutterKeyboardMode) {
Comment thread
vashworth marked this conversation as resolved.
Outdated
FlutterKeyboardModeHidden = 0,
FlutterKeyboardModeDocked = 1,
FlutterKeyboardModeFloating = 2,
};

- (void)keyboardWillShowNotification:(NSNotification*)notification {
// Immediately prior to a docked keyboard being shown or when a keyboard goes from
// undocked/floating to docked, this notification is triggered.
[self handleKeyboardNotification:notification notificationName:UIKeyboardWillShowNotification];
Comment thread
vashworth marked this conversation as resolved.
Outdated
}

- (void)keyboardWillChangeFrame:(NSNotification*)notification {
NSDictionary* info = [notification userInfo];
// Immediately prior to a change in keyboard frame, this notification is triggered.
// There are some cases where UIKeyboardWillShowNotification & UIKeyboardWillHideNotification
// do not act as expected and this is used to catch those cases.
[self handleKeyboardNotification:notification
notificationName:UIKeyboardWillChangeFrameNotification];
}

// Ignore keyboard notifications related to other apps.
id isLocal = info[UIKeyboardIsLocalUserInfoKey];
if (isLocal && ![isLocal boolValue]) {
return;
}
- (void)keyboardWillBeHidden:(NSNotification*)notification {
// When keyboard is hidden or undocked, this notification will be triggered.
Comment thread
vashworth marked this conversation as resolved.
[self handleKeyboardNotification:notification notificationName:UIKeyboardWillHideNotification];
}

// Ignore keyboard notifications if engine’s viewController is not current viewController.
if ([_engine.get() viewController] != self) {
- (void)handleKeyboardNotification:(NSNotification*)notification
notificationName:(NSNotificationName)notificationName {
BOOL ignoreNotification = [self shouldIgnoreKeyboardNotification:notification
notificationName:notificationName];
if (ignoreNotification) {
Comment thread
vashworth marked this conversation as resolved.
Outdated
return;
}

NSDictionary* info = notification.userInfo;
CGRect keyboardFrame = [[info objectForKey:UIKeyboardFrameEndUserInfoKey] CGRectValue];
Comment thread
vashworth marked this conversation as resolved.
Outdated
CGRect screenRect = [[UIScreen mainScreen] bounds];
FlutterKeyboardMode keyboardMode = [self calculateKeyboardAttachMode:keyboardFrame
notificationName:notificationName];
CGFloat calculatedInset = [self calculateKeyboardInset:keyboardFrame keyboardMode:keyboardMode];

// Get the animation duration
// Avoid double triggering startKeyBoardAnimation.
Comment thread
vashworth marked this conversation as resolved.
if (self.targetViewInsetBottom == calculatedInset) {
return;
}

self.targetViewInsetBottom = calculatedInset;
NSTimeInterval duration =
[[info objectForKey:UIKeyboardAnimationDurationUserInfoKey] doubleValue];

// Considering the iPad's split keyboard, Flutter needs to check if the keyboard frame is present
// in the screen to see if the keyboard is visible.
if (CGRectIntersectsRect(keyboardFrame, screenRect)) {
CGFloat bottom = CGRectGetHeight(keyboardFrame);
CGFloat scale = [UIScreen mainScreen].scale;
// The keyboard is treated as an inset since we want to effectively reduce the window size by
// the keyboard height. The Dart side will compute a value accounting for the keyboard-consuming
// bottom padding.
self.targetViewInsetBottom = bottom * scale;
} else {
self.targetViewInsetBottom = 0;
}
[self startKeyBoardAnimation:duration];
}

- (void)keyboardWillBeHidden:(NSNotification*)notification {
NSDictionary* info = [notification userInfo];
- (BOOL)shouldIgnoreKeyboardNotification:(NSNotification*)notification
notificationName:(NSNotificationName)notificationName {
Comment thread
vashworth marked this conversation as resolved.
Outdated
BOOL isKeyboardNotificationForThisView = [self isKeyboardNotificationForThisView:notification];
BOOL isKeyboardRotated = [self isKeyboardRotated:notification];
Comment thread
vashworth marked this conversation as resolved.
Outdated

// Don't ignore UIKeyboardWillHideNotification notifications.
Comment thread
vashworth marked this conversation as resolved.
if (notificationName == UIKeyboardWillHideNotification) {
// Skip hide notification when rotation in progress unless triggered by another app.
Comment thread
vashworth marked this conversation as resolved.
Outdated
if (isKeyboardRotated && isKeyboardNotificationForThisView) {
return YES;
}
return NO;
}

// Ignore notification when keyboard's dimensions and position are all zeroes,
// for UIKeyboardWillChangeFrameNotification. This happens when keyboard is dragged.
NSDictionary* info = notification.userInfo;
CGRect keyboardFrame = [[info objectForKey:UIKeyboardFrameEndUserInfoKey] CGRectValue];
Comment thread
vashworth marked this conversation as resolved.
Outdated
if (notificationName == UIKeyboardWillChangeFrameNotification &&
CGRectEqualToRect(keyboardFrame, CGRectZero)) {
return YES;
}

// Don't ignore other times a keyboard's height or width is 0.
Comment thread
vashworth marked this conversation as resolved.
Outdated
if (CGRectIsEmpty(keyboardFrame)) {
return NO;
}

// Ignore keyboard notifications related to other apps.
Comment thread
vashworth marked this conversation as resolved.
Outdated
if (!isKeyboardNotificationForThisView) {
return YES;
}

// Ignore notification when keyboard is in process of being rotated.
if (isKeyboardRotated) {
return YES;
}

return NO;
}

- (BOOL)isKeyboardNotificationForThisView:(NSNotification*)notification {
NSDictionary* info = notification.userInfo;
// Keyboard notifications related to other apps.
id isLocal = info[UIKeyboardIsLocalUserInfoKey];
if (isLocal && ![isLocal boolValue]) {
return;
return NO;
}

// Ignore keyboard notifications if engine’s viewController is not current viewController.
// Engine’s viewController is not current viewController.
if ([_engine.get() viewController] != self) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

does this happen when an app shows multiple flutter view controller on the same screen?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, from my understanding this can happen when you're using add-to-app stuff like this flutter/flutter#39036 (comment). This is a good point, though. I didn't test use case like that.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Update: I did test this use case manually and everything appeared to be working correctly.

return;
return NO;
}
return YES;
}

- (BOOL)isKeyboardRotated:(NSNotification*)notification {
// When the keyboard's width at the beginning of the animation equals the screen's
// current height, we can assume the keyboard was rotated.
Comment thread
vashworth marked this conversation as resolved.
Outdated
NSDictionary* info = notification.userInfo;
CGRect screenRect = [self getMainScreen].bounds;
CGFloat screenHeight = CGRectGetHeight(screenRect);
CGRect keyboardEndFrame = [[info objectForKey:UIKeyboardFrameEndUserInfoKey] CGRectValue];
CGRect keyboardBeginFrame = [[info objectForKey:UIKeyboardFrameBeginUserInfoKey] CGRectValue];
CGFloat keyboardBeginWidth = CGRectGetWidth(keyboardBeginFrame);
if (CGRectEqualToRect(keyboardEndFrame, CGRectZero)) {
return NO;
}
if (screenHeight == keyboardBeginWidth) {
return YES;
}
return NO;
}

if (self.targetViewInsetBottom != 0) {
// Ensure the keyboard will be dismissed. Just like the keyboardWillChangeFrame,
// keyboardWillBeHidden is also in an animation block in iOS sdk, so we don't need to set the
// animation curve. Related issue: https://github.com/flutter/flutter/issues/99951
self.targetViewInsetBottom = 0;
NSTimeInterval duration =
[[info objectForKey:UIKeyboardAnimationDurationUserInfoKey] doubleValue];
[self startKeyBoardAnimation:duration];
- (FlutterKeyboardMode)calculateKeyboardAttachMode:(CGRect)keyboardFrame
notificationName:(NSNotificationName)notificationName {
// There are multiple types of keyboard: docked, undocked, split, split docked,
// floating, predictive-only, minimized. This function will categorize
// the keyboard as one of the following modes: docked, floating, or hidden.
// Docked mode includes docked, split docked, predictive-only (when opening via click),
// and minimized (when opened via click).
// Floating includes undocked, split, floating, predictive-only (when dragged and dropped),
// and minimized (when dragged and dropped).
if (notificationName == UIKeyboardWillHideNotification) {
return FlutterKeyboardModeHidden;
}

// If keyboard's dimensions and position are all zeroes,
// that means it's been dragged and therefore floating.
if (CGRectEqualToRect(keyboardFrame, CGRectZero)) {
return FlutterKeyboardModeFloating;
}
// If keyboard's width or height are 0, it's hidden.
if (CGRectIsEmpty(keyboardFrame)) {
return FlutterKeyboardModeHidden;
}

CGRect screenRect = [self getMainScreen].bounds;
CGFloat keyboardWidth = CGRectGetWidth(keyboardFrame);
CGFloat screenWidth = CGRectGetWidth(screenRect);

// If keyboard is not full width, it's floating.
if (keyboardWidth != screenWidth) {
return FlutterKeyboardModeFloating;
}

CGFloat screenHeight = CGRectGetHeight(screenRect);
CGRect adjustedKeyboardFrame = keyboardFrame;
adjustedKeyboardFrame.origin.y += [self calculateMultitaskingAdjustment:screenRect
keyboardFrame:keyboardFrame];
CGFloat adjustedKeyboardBottom = CGRectGetMaxY(adjustedKeyboardFrame);

// If the keyboard is above the bottom of the screen, it's floating.
if (adjustedKeyboardBottom < screenHeight) {
return FlutterKeyboardModeFloating;
}
// If the keyboard is partially or fully showing at the bottom of the screen, it's docked.
if (CGRectIntersectsRect(adjustedKeyboardFrame, screenRect)) {
return FlutterKeyboardModeDocked;
}
Comment thread
luckysmg marked this conversation as resolved.
Outdated
return FlutterKeyboardModeHidden;
}

- (CGFloat)calculateMultitaskingAdjustment:(CGRect)screenRect keyboardFrame:(CGRect)keyboardFrame {
// In Slide Over mode, the keyboard's frame does not include the space
// below the app, even though the keyboard may be at the bottom of the screen.
// To handle, shift the Y origin by the amount of space below the app.
if (self.view.traitCollection.userInterfaceIdiom == UIUserInterfaceIdiomPad &&
self.view.traitCollection.horizontalSizeClass == UIUserInterfaceSizeClassCompact &&
self.view.traitCollection.verticalSizeClass == UIUserInterfaceSizeClassRegular) {
CGFloat screenHeight = CGRectGetHeight(screenRect);
CGFloat keyboardBottom = CGRectGetMaxY(keyboardFrame);

// Stage Manager mode will also meet the above parameters, but it does not handle
// the keyboard positioning the same way, so skip if keyboard is at bottom of page.
if (screenHeight == keyboardBottom) {
return 0;
}
CGRect viewFrameInScreen = [self.view convertRect:self.view.frame
toCoordinateSpace:[self getMainScreen].coordinateSpace];
CGFloat viewBottom = CGRectGetMaxY(viewFrameInScreen);
CGFloat offset = screenHeight - viewBottom;
if (offset > 0) {
return offset;
}
}
return 0;
}

- (CGFloat)calculateKeyboardInset:(CGRect)keyboardFrame keyboardMode:(NSInteger)keyboardMode {
// Only docked keyboards will have an inset.
if (keyboardMode == FlutterKeyboardModeDocked) {
// Calculate how much of the keyboard intersects with the view.
CGRect viewFrameInScreen = [self.view convertRect:self.view.frame
toCoordinateSpace:[self getMainScreen].coordinateSpace];
CGRect intersection = CGRectIntersection(keyboardFrame, viewFrameInScreen);
CGFloat portionOfKeyboardInView = CGRectGetHeight(intersection);

// The keyboard is treated as an inset since we want to effectively reduce the window size by
// the keyboard height. The Dart side will compute a value accounting for the keyboard-consuming
// bottom padding.
CGFloat scale = [self getMainScreen].scale;
return portionOfKeyboardInView * scale;
}
return 0;
}

- (void)startKeyBoardAnimation:(NSTimeInterval)duration {
Expand Down
Loading