diff --git a/shell/platform/darwin/ios/framework/Source/FlutterPlatformPlugin.mm b/shell/platform/darwin/ios/framework/Source/FlutterPlatformPlugin.mm index c1d7eaea3296e..4739c3119fc23 100644 --- a/shell/platform/darwin/ios/framework/Source/FlutterPlatformPlugin.mm +++ b/shell/platform/darwin/ios/framework/Source/FlutterPlatformPlugin.mm @@ -10,6 +10,8 @@ #import #include "flutter/fml/logging.h" +#import "flutter/shell/platform/darwin/ios/framework/Source/FlutterEngine_Internal.h" +#import "flutter/shell/platform/darwin/ios/framework/Source/FlutterTextInputPlugin.h" #import "flutter/shell/platform/darwin/ios/framework/Source/FlutterViewController_Internal.h" #import "flutter/shell/platform/darwin/ios/framework/Source/UIViewController+FlutterScreenAndSceneIfLoaded.h" @@ -154,10 +156,38 @@ - (void)handleMethodCall:(FlutterMethodCall*)call result:(FlutterResult)result { - (void)showShareViewController:(NSString*)content { UIViewController* engineViewController = [_engine.get() viewController]; + NSArray* itemsToShare = @[ content ?: [NSNull null] ]; UIActivityViewController* activityViewController = [[[UIActivityViewController alloc] initWithActivityItems:itemsToShare applicationActivities:nil] autorelease]; + + if (UI_USER_INTERFACE_IDIOM() == UIUserInterfaceIdiomPad) { + // On iPad, the share screen is presented in a popover view, and requires a + // sourceView and sourceRect + FlutterTextInputPlugin* _textInputPlugin = [_engine.get() textInputPlugin]; + UITextRange* range = _textInputPlugin.textInputView.selectedTextRange; + + // firstRectForRange cannot be used here as it's current implementation does + // not always return the full rect of the range. + CGRect firstRect = [(FlutterTextInputView*)_textInputPlugin.textInputView + caretRectForPosition:(FlutterTextPosition*)range.start]; + CGRect transformedFirstRect = [(FlutterTextInputView*)_textInputPlugin.textInputView + localRectFromFrameworkTransform:firstRect]; + CGRect lastRect = [(FlutterTextInputView*)_textInputPlugin.textInputView + caretRectForPosition:(FlutterTextPosition*)range.end]; + CGRect transformedLastRect = [(FlutterTextInputView*)_textInputPlugin.textInputView + localRectFromFrameworkTransform:lastRect]; + + activityViewController.popoverPresentationController.sourceView = engineViewController.view; + // In case of RTL Language, get the minimum x coordinate + activityViewController.popoverPresentationController.sourceRect = + CGRectMake(fmin(transformedFirstRect.origin.x, transformedLastRect.origin.x), + transformedFirstRect.origin.y, + abs(transformedLastRect.origin.x - transformedFirstRect.origin.x), + transformedFirstRect.size.height); + } + [engineViewController presentViewController:activityViewController animated:YES completion:nil]; } diff --git a/shell/platform/darwin/ios/framework/Source/FlutterPlatformPluginTest.mm b/shell/platform/darwin/ios/framework/Source/FlutterPlatformPluginTest.mm index 894e1f30cd5e1..25907899edbdb 100644 --- a/shell/platform/darwin/ios/framework/Source/FlutterPlatformPluginTest.mm +++ b/shell/platform/darwin/ios/framework/Source/FlutterPlatformPluginTest.mm @@ -166,6 +166,44 @@ - (void)testShareScreenInvoked { [self waitForExpectationsWithTimeout:1 handler:nil]; } +- (void)testShareScreenInvokedOnIPad { + FlutterEngine* engine = [[FlutterEngine alloc] initWithName:@"test" project:nil]; + [engine runWithEntrypoint:nil]; + std::unique_ptr> _weakFactory = + std::make_unique>(engine); + + XCTestExpectation* presentExpectation = + [self expectationWithDescription:@"Share view controller presented on iPad"]; + + FlutterViewController* engineViewController = [[FlutterViewController alloc] initWithEngine:engine + nibName:nil + bundle:nil]; + FlutterViewController* mockEngineViewController = OCMPartialMock(engineViewController); + OCMStub([mockEngineViewController + presentViewController:[OCMArg isKindOfClass:[UIActivityViewController class]] + animated:YES + completion:nil]); + + id mockTraitCollection = OCMClassMock([UITraitCollection class]); + OCMStub([mockTraitCollection userInterfaceIdiom]).andReturn(UIUserInterfaceIdiomPad); + + FlutterPlatformPlugin* plugin = + [[FlutterPlatformPlugin alloc] initWithEngine:_weakFactory->GetWeakNSObject()]; + FlutterPlatformPlugin* mockPlugin = OCMPartialMock(plugin); + + FlutterMethodCall* methodCall = [FlutterMethodCall methodCallWithMethodName:@"Share.invoke" + arguments:@"Test"]; + FlutterResult result = ^(id result) { + OCMVerify([mockEngineViewController + presentViewController:[OCMArg isKindOfClass:[UIActivityViewController class]] + animated:YES + completion:nil]); + [presentExpectation fulfill]; + }; + [mockPlugin handleMethodCall:methodCall result:result]; + [self waitForExpectationsWithTimeout:1 handler:nil]; +} + - (void)testClipboardHasCorrectStrings { [UIPasteboard generalPasteboard].string = nil; FlutterEngine* engine = [[FlutterEngine alloc] initWithName:@"test" project:nil]; diff --git a/shell/platform/darwin/ios/framework/Source/FlutterTextInputPlugin.h b/shell/platform/darwin/ios/framework/Source/FlutterTextInputPlugin.h index a7a996cb39995..fe26c40c4fca5 100644 --- a/shell/platform/darwin/ios/framework/Source/FlutterTextInputPlugin.h +++ b/shell/platform/darwin/ios/framework/Source/FlutterTextInputPlugin.h @@ -163,6 +163,10 @@ FLUTTER_DARWIN_EXPORT - (instancetype)initWithFrame:(CGRect)frame NS_UNAVAILABLE; - (instancetype)initWithOwner:(FlutterTextInputPlugin*)textInputPlugin NS_DESIGNATED_INITIALIZER; +// TODO(louisehsu): These are being exposed to support Share in FlutterPlatformPlugin +// Consider moving that feature into FlutterTextInputPlugin to avoid exposing extra methods +- (CGRect)localRectFromFrameworkTransform:(CGRect)incomingRect; +- (CGRect)caretRectForPosition:(UITextPosition*)position; @end @interface UIView (FindFirstResponder)