From e2970df999e779d0da5fba2d04477cd206ae70a0 Mon Sep 17 00:00:00 2001 From: hellohuanlin <41930132+hellohuanlin@users.noreply.github.com> Date: Fri, 13 Dec 2024 11:25:25 -0800 Subject: [PATCH] [ios]limit web view not tappable workaround to a limited depth (flutter/engine#57193) This PR limits the search depth, because we don't want to enable this workaround for AdMob banner, which has a WKWebView in the depth of 7. See the previous PR for more context: https://github.com/flutter/engine/pull/57168 I was able to confirm that this returns YES for the 3P plugin, and NO for AdMob. *List which issues are fixed by this PR. You must list at least one issue.* https://github.com/flutter/flutter/issues/158961 *If you had to change anything in the [flutter/tests] repo, include a link to the migration guide as per the [breaking change policy].* [C++, Objective-C, Java style guides]: https://github.com/flutter/engine/blob/main/CONTRIBUTING.md#style --- .../framework/Source/FlutterPlatformViews.mm | 15 ++- .../Source/FlutterPlatformViewsTest.mm | 115 ++++++++++++++++++ 2 files changed, 127 insertions(+), 3 deletions(-) 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 56e6898e44..16ce9ba0dd 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,12 +566,15 @@ static BOOL _preparedOnce = NO; 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; } } @@ -593,7 +596,13 @@ 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 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]; } 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 1356e70072..98bbf6b85e 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 @@ -74,6 +74,9 @@ const float kFloatCompareEpsilon = 0.001; self.viewCreated = YES; } +- (void)dealloc { + gMockPlatformView = nil; +} @end @interface FlutterPlatformViewsTestMockFlutterPlatformFactory @@ -115,6 +118,10 @@ const float kFloatCompareEpsilon = 0.001; } self.viewCreated = YES; } + +- (void)dealloc { + gMockPlatformView = nil; +} @end @interface FlutterPlatformViewsTestMockWebViewFactory : NSObject @@ -167,6 +174,10 @@ const float kFloatCompareEpsilon = 0.001; } self.viewCreated = YES; } + +- (void)dealloc { + gMockPlatformView = nil; +} @end @interface FlutterPlatformViewsTestMockWrapperWebViewFactory : NSObject @@ -180,6 +191,49 @@ const float kFloatCompareEpsilon = 0.001; } @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 { @@ -3258,6 +3312,67 @@ fml::RefPtr GetDefaultTaskRunner() { } } +- (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;