diff --git a/engine/src/flutter/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews.mm b/engine/src/flutter/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews.mm index a036668fce..56e6898e44 100644 --- a/engine/src/flutter/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews.mm +++ b/engine/src/flutter/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews.mm @@ -566,6 +566,18 @@ static BOOL _preparedOnce = NO; self.delayingRecognizer.state = UIGestureRecognizerStateFailed; } +- (BOOL)containsWebView:(UIView*)view { + if ([view isKindOfClass:[WKWebView class]]) { + return YES; + } + for (UIView* subview in view.subviews) { + if ([self containsWebView:subview]) { + return YES; + } + } + return NO; +} + - (void)blockGesture { switch (_blockingPolicy) { case FlutterPlatformViewGestureRecognizersBlockingPolicyEager: @@ -581,7 +593,7 @@ static BOOL _preparedOnce = NO; // 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]]) { + if ([self containsWebView:self.embeddedView]) { [self removeGestureRecognizer:self.delayingRecognizer]; [self addGestureRecognizer:self.delayingRecognizer]; } diff --git a/engine/src/flutter/shell/platform/darwin/ios/framework/Source/FlutterPlatformViewsTest.mm b/engine/src/flutter/shell/platform/darwin/ios/framework/Source/FlutterPlatformViewsTest.mm index 1ca19a3fbe..1356e70072 100644 --- a/engine/src/flutter/shell/platform/darwin/ios/framework/Source/FlutterPlatformViewsTest.mm +++ b/engine/src/flutter/shell/platform/darwin/ios/framework/Source/FlutterPlatformViewsTest.mm @@ -140,6 +140,46 @@ const float kFloatCompareEpsilon = 0.001; @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; +} +@end + +@interface FlutterPlatformViewsTestMockWrapperWebViewFactory : NSObject +@end + +@implementation FlutterPlatformViewsTestMockWrapperWebViewFactory +- (NSObject*)createWithFrame:(CGRect)frame + viewIdentifier:(int64_t)viewId + arguments:(id _Nullable)args { + return [[FlutterPlatformViewsTestMockWrapperWebView alloc] init]; +} +@end + namespace flutter { namespace { class FlutterPlatformViewsTestMockPlatformViewDelegate : public PlatformView::Delegate { @@ -3153,6 +3193,71 @@ fml::RefPtr GetDefaultTaskRunner() { } } +- (void) + testFlutterPlatformViewBlockGestureUnderEagerPolicyShouldRemoveAndAddBackDelayingRecognizerForWrapperWebView { + flutter::FlutterPlatformViewsTestMockPlatformViewDelegate mock_delegate; + + flutter::TaskRunners runners(/*label=*/self.name.UTF8String, + /*platform=*/GetDefaultTaskRunner(), + /*raster=*/GetDefaultTaskRunner(), + /*ui=*/GetDefaultTaskRunner(), + /*io=*/GetDefaultTaskRunner()); + FlutterPlatformViewsController* flutterPlatformViewsController = + [[FlutterPlatformViewsController alloc] init]; + flutterPlatformViewsController.taskRunner = 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 + withId:@"MockWrapperWebView" + gestureRecognizersBlockingPolicy:FlutterPlatformViewGestureRecognizersBlockingPolicyEager]; + FlutterResult result = ^(id result) { + }; + [flutterPlatformViewsController + onMethodCall:[FlutterMethodCall + methodCallWithMethodName:@"create" + arguments:@{@"id" : @2, @"viewType" : @"MockWrapperWebView"}] + result: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) testFlutterPlatformViewBlockGestureUnderEagerPolicyShouldNotRemoveAndAddBackDelayingRecognizerForNonWebView { flutter::FlutterPlatformViewsTestMockPlatformViewDelegate mock_delegate;