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 12 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
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,11 @@ @implementation FlutterPlatformPluginTest

- (void)testClipboardHasCorrectStrings {
[UIPasteboard generalPasteboard].string = nil;
FlutterEngine* engine = [[FlutterEngine alloc] initWithName:@"test" project:nil];
FlutterEngine* engine = [[[FlutterEngine alloc] initWithName:@"test" project:nil] autorelease];
std::unique_ptr<fml::WeakPtrFactory<FlutterEngine>> _weakFactory =
std::make_unique<fml::WeakPtrFactory<FlutterEngine>>(engine);
FlutterPlatformPlugin* plugin =
[[FlutterPlatformPlugin alloc] initWithEngine:_weakFactory->GetWeakPtr()];
[[[FlutterPlatformPlugin alloc] initWithEngine:_weakFactory->GetWeakPtr()] autorelease];

XCTestExpectation* setStringExpectation = [self expectationWithDescription:@"setString"];
FlutterResult resultSet = ^(id result) {
Expand Down Expand Up @@ -61,11 +61,11 @@ - (void)testClipboardHasCorrectStrings {

- (void)testClipboardSetDataToNullDoNotCrash {
[UIPasteboard generalPasteboard].string = nil;
FlutterEngine* engine = [[FlutterEngine alloc] initWithName:@"test" project:nil];
FlutterEngine* engine = [[[FlutterEngine alloc] initWithName:@"test" project:nil] autorelease];
std::unique_ptr<fml::WeakPtrFactory<FlutterEngine>> _weakFactory =
std::make_unique<fml::WeakPtrFactory<FlutterEngine>>(engine);
FlutterPlatformPlugin* plugin =
[[FlutterPlatformPlugin alloc] initWithEngine:_weakFactory->GetWeakPtr()];
[[[FlutterPlatformPlugin alloc] initWithEngine:_weakFactory->GetWeakPtr()] autorelease];

XCTestExpectation* setStringExpectation = [self expectationWithDescription:@"setData"];
FlutterResult resultSet = ^(id result) {
Expand All @@ -88,18 +88,18 @@ - (void)testClipboardSetDataToNullDoNotCrash {
}

- (void)testPopSystemNavigator {
FlutterEngine* engine = [[FlutterEngine alloc] initWithName:@"test" project:nil];
FlutterEngine* engine = [[[FlutterEngine alloc] initWithName:@"test" project:nil] autorelease];
[engine runWithEntrypoint:nil];
FlutterViewController* flutterViewController =
[[FlutterViewController alloc] initWithEngine:engine nibName:nil bundle:nil];
UINavigationController* navigationController =
[[UINavigationController alloc] initWithRootViewController:flutterViewController];
UITabBarController* tabBarController = [[UITabBarController alloc] init];
UINavigationController* navigationController = [[[UINavigationController alloc]
initWithRootViewController:flutterViewController] autorelease];
UITabBarController* tabBarController = [[[UITabBarController alloc] init] autorelease];
tabBarController.viewControllers = @[ navigationController ];
std::unique_ptr<fml::WeakPtrFactory<FlutterEngine>> _weakFactory =
std::make_unique<fml::WeakPtrFactory<FlutterEngine>>(engine);
FlutterPlatformPlugin* plugin =
[[FlutterPlatformPlugin alloc] initWithEngine:_weakFactory->GetWeakPtr()];
[[[FlutterPlatformPlugin alloc] initWithEngine:_weakFactory->GetWeakPtr()] autorelease];

id navigationControllerMock = OCMPartialMock(navigationController);
OCMStub([navigationControllerMock popViewControllerAnimated:YES]);
Expand All @@ -113,16 +113,19 @@ - (void)testPopSystemNavigator {
[plugin handleMethodCall:methodCallSet result:resultSet];
[self waitForExpectationsWithTimeout:1 handler:nil];
OCMVerify([navigationControllerMock popViewControllerAnimated:YES]);

[flutterViewController deregisterNotifications];
[flutterViewController release];
}

- (void)testWhetherDeviceHasLiveTextInputInvokeCorrectly {
FlutterEngine* engine = [[FlutterEngine alloc] initWithName:@"test" project:nil];
FlutterEngine* engine = [[[FlutterEngine alloc] initWithName:@"test" project:nil] autorelease];
std::unique_ptr<fml::WeakPtrFactory<FlutterEngine>> _weakFactory =
std::make_unique<fml::WeakPtrFactory<FlutterEngine>>(engine);
XCTestExpectation* invokeExpectation =
[self expectationWithDescription:@"isLiveTextInputAvailableInvoke"];
FlutterPlatformPlugin* plugin =
[[FlutterPlatformPlugin alloc] initWithEngine:_weakFactory->GetWeakPtr()];
[[[FlutterPlatformPlugin alloc] initWithEngine:_weakFactory->GetWeakPtr()] autorelease];
Copy link
Contributor

Choose a reason for hiding this comment

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

thanks for fixing all of these.

FlutterPlatformPlugin* mockPlugin = OCMPartialMock(plugin);
FlutterMethodCall* methodCall =
[FlutterMethodCall methodCallWithMethodName:@"LiveText.isLiveTextInputAvailable"
Expand Down
208 changes: 170 additions & 38 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*)mainScreen {
if (@available(iOS 13.0, *)) {
return self.viewIfLoaded.window.windowScene.screen;
}
return UIScreen.mainScreen;
}

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

#pragma mark - Keyboard events

- (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];
}

- (void)keyboardWillChangeFrame:(NSNotification*)notification {
NSDictionary* info = [notification userInfo];
// Immediately prior to a change in keyboard frame, this notification is triggered.
[self handleKeyboardNotification:notification];
}

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

- (void)handleKeyboardNotification:(NSNotification*)notification {
// See https:://flutter.dev/go/ios-keyboard-calculating-inset for more details
// on why notifications are used and how things are calculated.
if ([self shouldIgnoreKeyboardNotification:notification]) {
return;
}

// Ignore keyboard notifications if engine’s viewController is not current viewController.
if ([_engine.get() viewController] != self) {
NSDictionary* info = notification.userInfo;
CGRect keyboardFrame = [info[UIKeyboardFrameEndUserInfoKey] CGRectValue];
FlutterKeyboardMode keyboardMode = [self calculateKeyboardAttachMode:notification];
CGFloat calculatedInset = [self calculateKeyboardInset:keyboardFrame keyboardMode:keyboardMode];

// Avoid double triggering startKeyBoardAnimation.
if (self.targetViewInsetBottom == calculatedInset) {
return;
}

CGRect keyboardFrame = [[info objectForKey:UIKeyboardFrameEndUserInfoKey] CGRectValue];
CGRect screenRect = [[UIScreen mainScreen] bounds];
self.targetViewInsetBottom = calculatedInset;
NSTimeInterval duration = [info[UIKeyboardAnimationDurationUserInfoKey] doubleValue];
[self startKeyBoardAnimation:duration];
}

// Get the animation duration
NSTimeInterval duration =
[[info objectForKey:UIKeyboardAnimationDurationUserInfoKey] doubleValue];
- (BOOL)shouldIgnoreKeyboardNotification:(NSNotification*)notification {
// Don't ignore UIKeyboardWillHideNotification notifications.
if (notification.name == UIKeyboardWillHideNotification) {
return NO;
}

// 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;
// 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[UIKeyboardFrameEndUserInfoKey] CGRectValue];
if (notification.name == UIKeyboardWillChangeFrameNotification &&
CGRectEqualToRect(keyboardFrame, CGRectZero)) {
return YES;
}
[self startKeyBoardAnimation:duration];
}

- (void)keyboardWillBeHidden:(NSNotification*)notification {
NSDictionary* info = [notification userInfo];
// When keyboard's height/width is set to 0 by other app,
// do not ignore so that the inset will be set to 0.
if (CGRectIsEmpty(keyboardFrame)) {
return NO;
}

// Ignore keyboard notifications related to other apps.
if ([self isKeyboardNotificationForDifferentView:notification]) {
return YES;
}

// Ignore notification if the app is not active (meaning it's running in the
// background, interrupted, or the app is transitioning to or from the background).
if (UIApplication.sharedApplication.applicationState != UIApplicationStateActive) {
return YES;
}

return NO;
}

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

// 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
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
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
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 YES;
}
return NO;
}

- (FlutterKeyboardMode)calculateKeyboardAttachMode:(NSNotification*)notification {
// There are multiple types of keyboard: docked, undocked, split, split docked,
// floating, expanded shortcuts bar, minimized shortcuts bar. This function will categorize
// the keyboard as one of the following modes: docked, floating, or hidden.
// Docked mode includes docked, split docked, expanded shortcuts bar (when opening via click),
// and minimized shortcuts bar (when opened via click).
// Floating includes undocked, split, floating, expanded shortcuts bar (when dragged and dropped),
// and minimized shortcuts bar (when dragged and dropped).
NSDictionary* info = notification.userInfo;
CGRect keyboardFrame = [info[UIKeyboardFrameEndUserInfoKey] CGRectValue];

if (notification.name == 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 mainScreen].bounds;
CGRect adjustedKeyboardFrame = keyboardFrame;
adjustedKeyboardFrame.origin.y += [self calculateMultitaskingAdjustment:screenRect
keyboardFrame:keyboardFrame];

// If the keyboard is partially or fully showing within the screen, it's either docked or
// floating. Sometimes with custom keyboard extensions, the keyboard's position may be off by a
// small decimal amount. Round to compare.
CGRect intersection = CGRectIntersection(adjustedKeyboardFrame, screenRect);
CGFloat intersectionHeight = CGRectGetHeight(intersection);
CGFloat intersectionWidth = CGRectGetWidth(intersection);
if (round(intersectionHeight) > 0 && intersectionWidth > 0) {
// If the keyboard is above the bottom of the screen, it's floating.
CGFloat screenHeight = CGRectGetHeight(screenRect);
CGFloat adjustedKeyboardBottom = CGRectGetMaxY(adjustedKeyboardFrame);
if (round(adjustedKeyboardBottom) < screenHeight) {
return FlutterKeyboardModeFloating;
}
return FlutterKeyboardModeDocked;
}
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.viewIfLoaded.traitCollection.userInterfaceIdiom == UIUserInterfaceIdiomPad &&
self.viewIfLoaded.traitCollection.horizontalSizeClass == UIUserInterfaceSizeClassCompact &&
self.viewIfLoaded.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 viewRectRelativeToScreen =
[self.viewIfLoaded convertRect:self.viewIfLoaded.frame
toCoordinateSpace:[self mainScreen].coordinateSpace];
CGFloat viewBottom = CGRectGetMaxY(viewRectRelativeToScreen);
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 viewRectRelativeToScreen =
[self.viewIfLoaded convertRect:self.viewIfLoaded.frame
toCoordinateSpace:[self mainScreen].coordinateSpace];
CGRect intersection = CGRectIntersection(keyboardFrame, viewRectRelativeToScreen);
CGFloat portionOfKeyboardInView = CGRectGetHeight(intersection);

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];
// 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 mainScreen].scale;
return portionOfKeyboardInView * scale;
}
return 0;
}

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