diff --git a/shell/platform/darwin/ios/framework/Source/FlutterPlatformViewsTest.mm b/shell/platform/darwin/ios/framework/Source/FlutterPlatformViewsTest.mm index 3faf41321e9fb..b91fde939a861 100644 --- a/shell/platform/darwin/ios/framework/Source/FlutterPlatformViewsTest.mm +++ b/shell/platform/darwin/ios/framework/Source/FlutterPlatformViewsTest.mm @@ -69,6 +69,9 @@ - (void)checkViewCreatedOnce { self.viewCreated = YES; } +- (void)dealloc { + gMockPlatformView = nil; +} @end @interface FlutterPlatformViewsTestMockFlutterPlatformFactory @@ -110,6 +113,10 @@ - (void)checkViewCreatedOnce { } self.viewCreated = YES; } + +- (void)dealloc { + gMockPlatformView = nil; +} @end @interface FlutterPlatformViewsTestMockWebViewFactory : NSObject @@ -135,6 +142,93 @@ @implementation FlutterPlatformViewsTestNilFlutterPlatformFactory @end +@interface FlutterPlatformViewsTestMockWrapperWebView : NSObject +@property(nonatomic, strong) UIView* view; +@property(nonatomic, assign) BOOL viewCreated; +@end + +@implementation FlutterPlatformViewsTestMockWrapperWebView +- (instancetype)init { + if (self = [super init]) { + _view = [[UIView alloc] init]; + [_view addSubview:[[WKWebView alloc] init]]; + gMockPlatformView = _view; + _viewCreated = NO; + } + return self; +} + +- (UIView*)view { + [self checkViewCreatedOnce]; + return _view; +} + +- (void)checkViewCreatedOnce { + if (self.viewCreated) { + abort(); + } + self.viewCreated = YES; +} + +- (void)dealloc { + gMockPlatformView = nil; +} +@end + +@interface FlutterPlatformViewsTestMockWrapperWebViewFactory : NSObject +@end + +@implementation FlutterPlatformViewsTestMockWrapperWebViewFactory +- (NSObject*)createWithFrame:(CGRect)frame + viewIdentifier:(int64_t)viewId + arguments:(id _Nullable)args { + return [[FlutterPlatformViewsTestMockWrapperWebView alloc] init]; +} +@end + +@interface FlutterPlatformViewsTestMockNestedWrapperWebView : NSObject +@property(nonatomic, strong) UIView* view; +@property(nonatomic, assign) BOOL viewCreated; +@end + +@implementation FlutterPlatformViewsTestMockNestedWrapperWebView +- (instancetype)init { + if (self = [super init]) { + _view = [[UIView alloc] init]; + UIView* childView = [[UIView alloc] init]; + [_view addSubview:childView]; + [childView addSubview:[[WKWebView alloc] init]]; + gMockPlatformView = _view; + _viewCreated = NO; + } + return self; +} + +- (UIView*)view { + [self checkViewCreatedOnce]; + return _view; +} + +- (void)checkViewCreatedOnce { + if (self.viewCreated) { + abort(); + } + self.viewCreated = YES; +} +@end + +@interface FlutterPlatformViewsTestMockNestedWrapperWebViewFactory + : NSObject +@end + +@implementation FlutterPlatformViewsTestMockNestedWrapperWebViewFactory +- (NSObject*)createWithFrame:(CGRect)frame + viewIdentifier:(int64_t)viewId + arguments:(id _Nullable)args { + return [[FlutterPlatformViewsTestMockNestedWrapperWebView alloc] init]; +} +@end + namespace flutter { namespace { class FlutterPlatformViewsTestMockPlatformViewDelegate : public PlatformView::Delegate { @@ -2883,6 +2977,125 @@ - (void)testFlutterPlatformViewTouchesCancelledEventAreForcedToBeCancelled { } } +- (void) + testFlutterPlatformViewBlockGestureUnderEagerPolicyShouldRemoveAndAddBackDelayingRecognizerForWrapperWebView { + flutter::FlutterPlatformViewsTestMockPlatformViewDelegate mock_delegate; + + flutter::TaskRunners runners(/*label=*/self.name.UTF8String, + /*platform=*/GetDefaultTaskRunner(), + /*raster=*/GetDefaultTaskRunner(), + /*ui=*/GetDefaultTaskRunner(), + /*io=*/GetDefaultTaskRunner()); + auto flutterPlatformViewsController = std::make_shared(); + flutterPlatformViewsController->SetTaskRunner(GetDefaultTaskRunner()); + auto platform_view = std::make_unique( + /*delegate=*/mock_delegate, + /*rendering_api=*/mock_delegate.settings_.enable_impeller + ? flutter::IOSRenderingAPI::kMetal + : flutter::IOSRenderingAPI::kSoftware, + /*platform_views_controller=*/flutterPlatformViewsController, + /*task_runners=*/runners, + /*worker_task_runner=*/nil, + /*is_gpu_disabled_jsync_switch=*/std::make_shared()); + + FlutterPlatformViewsTestMockWrapperWebViewFactory* factory = + [[FlutterPlatformViewsTestMockWrapperWebViewFactory alloc] init]; + flutterPlatformViewsController->RegisterViewFactory( + factory, @"MockWrapperWebView", FlutterPlatformViewGestureRecognizersBlockingPolicyEager); + FlutterResult result = ^(id result) { + }; + flutterPlatformViewsController->OnMethodCall( + [FlutterMethodCall + methodCallWithMethodName:@"create" + arguments:@{@"id" : @2, @"viewType" : @"MockWrapperWebView"}], + result); + + XCTAssertNotNil(gMockPlatformView); + + // Find touch inteceptor view + UIView* touchInteceptorView = gMockPlatformView; + while (touchInteceptorView != nil && + ![touchInteceptorView isKindOfClass:[FlutterTouchInterceptingView class]]) { + touchInteceptorView = touchInteceptorView.superview; + } + XCTAssertNotNil(touchInteceptorView); + + XCTAssert(touchInteceptorView.gestureRecognizers.count == 2); + UIGestureRecognizer* delayingRecognizer = touchInteceptorView.gestureRecognizers[0]; + UIGestureRecognizer* forwardingRecognizer = touchInteceptorView.gestureRecognizers[1]; + + XCTAssert([delayingRecognizer isKindOfClass:[FlutterDelayingGestureRecognizer class]]); + XCTAssert([forwardingRecognizer isKindOfClass:[ForwardingGestureRecognizer class]]); + + [(FlutterTouchInterceptingView*)touchInteceptorView blockGesture]; + + if (@available(iOS 18.2, *)) { + // Since we remove and add back delayingRecognizer, it would be reordered to the last. + XCTAssertEqual(touchInteceptorView.gestureRecognizers[0], forwardingRecognizer); + XCTAssertEqual(touchInteceptorView.gestureRecognizers[1], delayingRecognizer); + } else { + XCTAssertEqual(touchInteceptorView.gestureRecognizers[0], delayingRecognizer); + XCTAssertEqual(touchInteceptorView.gestureRecognizers[1], forwardingRecognizer); + } +} + +- (void) + testFlutterPlatformViewBlockGestureUnderEagerPolicyShouldNotRemoveAndAddBackDelayingRecognizerForNestedWrapperWebView { + flutter::FlutterPlatformViewsTestMockPlatformViewDelegate mock_delegate; + + flutter::TaskRunners runners(/*label=*/self.name.UTF8String, + /*platform=*/GetDefaultTaskRunner(), + /*raster=*/GetDefaultTaskRunner(), + /*ui=*/GetDefaultTaskRunner(), + /*io=*/GetDefaultTaskRunner()); + auto flutterPlatformViewsController = std::make_shared(); + flutterPlatformViewsController->SetTaskRunner(GetDefaultTaskRunner()); + auto platform_view = std::make_unique( + /*delegate=*/mock_delegate, + /*rendering_api=*/mock_delegate.settings_.enable_impeller + ? flutter::IOSRenderingAPI::kMetal + : flutter::IOSRenderingAPI::kSoftware, + /*platform_views_controller=*/flutterPlatformViewsController, + /*task_runners=*/runners, + /*worker_task_runner=*/nil, + /*is_gpu_disabled_jsync_switch=*/std::make_shared()); + + FlutterPlatformViewsTestMockNestedWrapperWebViewFactory* factory = + [[FlutterPlatformViewsTestMockNestedWrapperWebViewFactory alloc] init]; + flutterPlatformViewsController->RegisterViewFactory( + factory, @"MockNestedWrapperWebView", + FlutterPlatformViewGestureRecognizersBlockingPolicyEager); + FlutterResult result = ^(id result) { + }; + flutterPlatformViewsController->OnMethodCall( + [FlutterMethodCall + methodCallWithMethodName:@"create" + arguments:@{@"id" : @2, @"viewType" : @"MockNestedWrapperWebView"}], + result); + + XCTAssertNotNil(gMockPlatformView); + + // Find touch inteceptor view + UIView* touchInteceptorView = gMockPlatformView; + while (touchInteceptorView != nil && + ![touchInteceptorView isKindOfClass:[FlutterTouchInterceptingView class]]) { + touchInteceptorView = touchInteceptorView.superview; + } + XCTAssertNotNil(touchInteceptorView); + + XCTAssert(touchInteceptorView.gestureRecognizers.count == 2); + UIGestureRecognizer* delayingRecognizer = touchInteceptorView.gestureRecognizers[0]; + UIGestureRecognizer* forwardingRecognizer = touchInteceptorView.gestureRecognizers[1]; + + XCTAssert([delayingRecognizer isKindOfClass:[FlutterDelayingGestureRecognizer class]]); + XCTAssert([forwardingRecognizer isKindOfClass:[ForwardingGestureRecognizer class]]); + + [(FlutterTouchInterceptingView*)touchInteceptorView blockGesture]; + + XCTAssertEqual(touchInteceptorView.gestureRecognizers[0], delayingRecognizer); + XCTAssertEqual(touchInteceptorView.gestureRecognizers[1], forwardingRecognizer); +} + - (void) testFlutterPlatformViewBlockGestureUnderEagerPolicyShouldNotRemoveAndAddBackDelayingRecognizerForNonWebView { flutter::FlutterPlatformViewsTestMockPlatformViewDelegate mock_delegate; diff --git a/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews_Internal.mm b/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews_Internal.mm index 1439591c1157a..7071183caa3b7 100644 --- a/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews_Internal.mm +++ b/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews_Internal.mm @@ -553,6 +553,21 @@ - (void)releaseGesture { self.delayingRecognizer.state = UIGestureRecognizerStateFailed; } +- (BOOL)containsWebView:(UIView*)view remainingSubviewDepth:(int)remainingSubviewDepth { + if (remainingSubviewDepth < 0) { + return NO; + } + if ([view isKindOfClass:[WKWebView class]]) { + return YES; + } + for (UIView* subview in view.subviews) { + if ([self containsWebView:subview remainingSubviewDepth:remainingSubviewDepth - 1]) { + return YES; + } + } + return NO; +} + - (void)blockGesture { switch (_blockingPolicy) { case FlutterPlatformViewGestureRecognizersBlockingPolicyEager: @@ -568,7 +583,13 @@ - (void)blockGesture { // FlutterPlatformViewGestureRecognizersBlockingPolicyEager, but we should try it if a similar // issue arises for the other policy. if (@available(iOS 18.2, *)) { - if ([self.embeddedView isKindOfClass:[WKWebView class]]) { + // This workaround is designed for WKWebView only. The 1P web view plugin provides a + // WKWebView itself as the platform view. However, some 3P plugins provide wrappers of + // WKWebView instead. So we perform DFS to search the view hierarchy (with a depth limit). + // Passing a limit of 0 means only searching for platform view itself; Pass 1 to include its + // children as well, and so on. We should be conservative and start with a small number. The + // AdMob banner has a WKWebView at depth 7. + if ([self containsWebView:self.embeddedView remainingSubviewDepth:1]) { [self removeGestureRecognizer:self.delayingRecognizer]; [self addGestureRecognizer:self.delayingRecognizer]; }