From ea25499b0778cf76ae19aca057097e944307e361 Mon Sep 17 00:00:00 2001 From: Huan Lin Date: Thu, 12 Dec 2024 13:38:00 -0800 Subject: [PATCH 1/4] [ios]enable the webview non tappable workaround by checking views --- .../Source/FlutterPlatformViewsTest.mm | 105 ++++++++++++++++++ .../Source/FlutterPlatformViews_Internal.mm | 14 ++- 2 files changed, 118 insertions(+), 1 deletion(-) diff --git a/shell/platform/darwin/ios/framework/Source/FlutterPlatformViewsTest.mm b/shell/platform/darwin/ios/framework/Source/FlutterPlatformViewsTest.mm index 3faf41321e9fb..c6f2de4d65b92 100644 --- a/shell/platform/darwin/ios/framework/Source/FlutterPlatformViewsTest.mm +++ b/shell/platform/darwin/ios/framework/Source/FlutterPlatformViewsTest.mm @@ -135,6 +135,46 @@ @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; +} +@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 { @@ -2883,6 +2923,71 @@ - (void)testFlutterPlatformViewTouchesCancelledEventAreForcedToBeCancelled { } } +- (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; diff --git a/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews_Internal.mm b/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews_Internal.mm index 1439591c1157a..b5ad2314a2b87 100644 --- a/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews_Internal.mm +++ b/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews_Internal.mm @@ -553,6 +553,18 @@ - (void)releaseGesture { 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: @@ -568,7 +580,7 @@ - (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]]) { + if ([self containsWebView:self.embeddedView]) { [self removeGestureRecognizer:self.delayingRecognizer]; [self addGestureRecognizer:self.delayingRecognizer]; } From cf220b94b60e27b10496bd894b2b2b011d8f719b Mon Sep 17 00:00:00 2001 From: Huan Lin Date: Thu, 12 Dec 2024 15:37:51 -0800 Subject: [PATCH 2/4] fix code conflict --- .../Source/FlutterPlatformViewsTest.mm | 21 ++++++++----------- 1 file changed, 9 insertions(+), 12 deletions(-) diff --git a/shell/platform/darwin/ios/framework/Source/FlutterPlatformViewsTest.mm b/shell/platform/darwin/ios/framework/Source/FlutterPlatformViewsTest.mm index c6f2de4d65b92..6124ed56a4673 100644 --- a/shell/platform/darwin/ios/framework/Source/FlutterPlatformViewsTest.mm +++ b/shell/platform/darwin/ios/framework/Source/FlutterPlatformViewsTest.mm @@ -2932,9 +2932,8 @@ - (void)testFlutterPlatformViewTouchesCancelledEventAreForcedToBeCancelled { /*raster=*/GetDefaultTaskRunner(), /*ui=*/GetDefaultTaskRunner(), /*io=*/GetDefaultTaskRunner()); - FlutterPlatformViewsController* flutterPlatformViewsController = - [[FlutterPlatformViewsController alloc] init]; - flutterPlatformViewsController.taskRunner = 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 @@ -2947,17 +2946,15 @@ - (void)testFlutterPlatformViewTouchesCancelledEventAreForcedToBeCancelled { FlutterPlatformViewsTestMockWrapperWebViewFactory* factory = [[FlutterPlatformViewsTestMockWrapperWebViewFactory alloc] init]; - [flutterPlatformViewsController - registerViewFactory:factory - withId:@"MockWrapperWebView" - gestureRecognizersBlockingPolicy:FlutterPlatformViewGestureRecognizersBlockingPolicyEager]; + flutterPlatformViewsController->RegisterViewFactory( + factory, @"MockWrapperWebView", FlutterPlatformViewGestureRecognizersBlockingPolicyEager); FlutterResult result = ^(id result) { }; - [flutterPlatformViewsController - onMethodCall:[FlutterMethodCall - methodCallWithMethodName:@"create" - arguments:@{@"id" : @2, @"viewType" : @"MockWrapperWebView"}] - result:result]; + flutterPlatformViewsController->OnMethodCall( + [FlutterMethodCall + methodCallWithMethodName:@"create" + arguments:@{@"id" : @2, @"viewType" : @"MockWrapperWebView"}], + result); XCTAssertNotNil(gMockPlatformView); From 9181a4d2f82a1510ec917a8dfc205984680ea8bb Mon Sep 17 00:00:00 2001 From: Huan Lin Date: Fri, 13 Dec 2024 10:33:52 -0800 Subject: [PATCH 3/4] [ios]limit web view not tappable workaround to a limited subview depth --- .../Source/FlutterPlatformViewsTest.mm | 115 ++++++++++++++++++ .../Source/FlutterPlatformViews_Internal.mm | 15 ++- 2 files changed, 127 insertions(+), 3 deletions(-) diff --git a/shell/platform/darwin/ios/framework/Source/FlutterPlatformViewsTest.mm b/shell/platform/darwin/ios/framework/Source/FlutterPlatformViewsTest.mm index 6124ed56a4673..ca462676e157b 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 @@ -162,6 +169,10 @@ - (void)checkViewCreatedOnce { } self.viewCreated = YES; } + +- (void)dealloc { + gMockPlatformView = nil; +} @end @interface FlutterPlatformViewsTestMockWrapperWebViewFactory : NSObject @@ -175,6 +186,49 @@ @implementation FlutterPlatformViewsTestMockWrapperWebViewFactory } @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 { @@ -2985,6 +3039,67 @@ - (void)testFlutterPlatformViewTouchesCancelledEventAreForcedToBeCancelled { } } +- (void) + testFlutterPlatformViewBlockGestureUnderEagerPolicyShouldNotRemoveAndAddBackDelayingRecognizerForNestedWrapperWebView { + 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()); + + FlutterPlatformViewsTestMockNestedWrapperWebViewFactory* factory = + [[FlutterPlatformViewsTestMockNestedWrapperWebViewFactory alloc] init]; + [flutterPlatformViewsController + registerViewFactory:factory + withId:@"MockNestedWrapperWebView" + gestureRecognizersBlockingPolicy:FlutterPlatformViewGestureRecognizersBlockingPolicyEager]; + FlutterResult result = ^(id result) { + }; + [flutterPlatformViewsController + onMethodCall:[FlutterMethodCall methodCallWithMethodName:@"create" + arguments:@{ + @"id" : @2, + @"viewType" : @"MockNestedWrapperWebView" + }] + 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]; + + 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 b5ad2314a2b87..7071183caa3b7 100644 --- a/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews_Internal.mm +++ b/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews_Internal.mm @@ -553,12 +553,15 @@ - (void)releaseGesture { self.delayingRecognizer.state = UIGestureRecognizerStateFailed; } -- (BOOL)containsWebView:(UIView*)view { +- (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]) { + if ([self containsWebView:subview remainingSubviewDepth:remainingSubviewDepth - 1]) { return YES; } } @@ -580,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 containsWebView:self.embeddedView]) { + // 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]; } From 9ec93558e7062218446146e89c1b189736ccb666 Mon Sep 17 00:00:00 2001 From: Huan Lin Date: Fri, 13 Dec 2024 10:59:56 -0800 Subject: [PATCH 4/4] fix code conflict --- .../Source/FlutterPlatformViewsTest.mm | 24 ++++++++----------- 1 file changed, 10 insertions(+), 14 deletions(-) diff --git a/shell/platform/darwin/ios/framework/Source/FlutterPlatformViewsTest.mm b/shell/platform/darwin/ios/framework/Source/FlutterPlatformViewsTest.mm index ca462676e157b..b91fde939a861 100644 --- a/shell/platform/darwin/ios/framework/Source/FlutterPlatformViewsTest.mm +++ b/shell/platform/darwin/ios/framework/Source/FlutterPlatformViewsTest.mm @@ -3048,9 +3048,8 @@ - (void)testFlutterPlatformViewTouchesCancelledEventAreForcedToBeCancelled { /*raster=*/GetDefaultTaskRunner(), /*ui=*/GetDefaultTaskRunner(), /*io=*/GetDefaultTaskRunner()); - FlutterPlatformViewsController* flutterPlatformViewsController = - [[FlutterPlatformViewsController alloc] init]; - flutterPlatformViewsController.taskRunner = 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 @@ -3063,19 +3062,16 @@ - (void)testFlutterPlatformViewTouchesCancelledEventAreForcedToBeCancelled { FlutterPlatformViewsTestMockNestedWrapperWebViewFactory* factory = [[FlutterPlatformViewsTestMockNestedWrapperWebViewFactory alloc] init]; - [flutterPlatformViewsController - registerViewFactory:factory - withId:@"MockNestedWrapperWebView" - gestureRecognizersBlockingPolicy:FlutterPlatformViewGestureRecognizersBlockingPolicyEager]; + flutterPlatformViewsController->RegisterViewFactory( + factory, @"MockNestedWrapperWebView", + FlutterPlatformViewGestureRecognizersBlockingPolicyEager); FlutterResult result = ^(id result) { }; - [flutterPlatformViewsController - onMethodCall:[FlutterMethodCall methodCallWithMethodName:@"create" - arguments:@{ - @"id" : @2, - @"viewType" : @"MockNestedWrapperWebView" - }] - result:result]; + flutterPlatformViewsController->OnMethodCall( + [FlutterMethodCall + methodCallWithMethodName:@"create" + arguments:@{@"id" : @2, @"viewType" : @"MockNestedWrapperWebView"}], + result); XCTAssertNotNil(gMockPlatformView);