[cp][3.28] Combined CP for "workaround for non-tappable webview #56804" AND "enable the webview non tappable workaround by checking subviews recursively #57168" (#57176)
CP for 3 PRs: https://github.com/flutter/engine/pull/56804 and https://github.com/flutter/engine/pull/57168 and https://github.com/flutter/engine/pull/57193
This is for 3.28.
Since the previous PR was not merged, so I combined the 2 PRs to make it easier to merge.
*List which issues are fixed by this PR. You must list at least one issue.*
https://github.com/pichillilorenzo/flutter_inappwebview/issues/2415
*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
diff --git a/shell/platform/darwin/ios/BUILD.gn b/shell/platform/darwin/ios/BUILD.gn
index 4a0eeac..6020365 100644
--- a/shell/platform/darwin/ios/BUILD.gn
+++ b/shell/platform/darwin/ios/BUILD.gn
@@ -165,6 +165,7 @@
"CoreVideo.framework",
"IOSurface.framework",
"QuartzCore.framework",
+ "WebKit.framework",
"UIKit.framework",
]
if (flutter_runtime_mode == "profile" || flutter_runtime_mode == "debug") {
diff --git a/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews.mm b/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews.mm
index a0e140e..16ce9ba 100644
--- a/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews.mm
+++ b/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews.mm
@@ -4,6 +4,8 @@
#import "flutter/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews_Internal.h"
+#import <WebKit/WebKit.h>
+
#include "flutter/display_list/effects/dl_image_filter.h"
#include "flutter/fml/platform/darwin/cf_utils.h"
#import "flutter/shell/platform/darwin/ios/ios_surface.h"
@@ -564,11 +566,48 @@
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:
// We block all other gesture recognizers immediately in this policy.
self.delayingRecognizer.state = UIGestureRecognizerStateEnded;
+
+ // On iOS 18.2, WKWebView's internal recognizer likely caches the old state of its blocking
+ // recognizers (i.e. delaying recognizer), resulting in non-tappable links. See
+ // https://github.com/flutter/flutter/issues/158961. Removing and adding back the delaying
+ // recognizer solves the problem, possibly because UIKit notifies all the recognizers related
+ // to (blocking or blocked by) this recognizer. It is not possible to inject this workaround
+ // from the web view plugin level. Right now we only observe this issue for
+ // FlutterPlatformViewGestureRecognizersBlockingPolicyEager, but we should try it if a similar
+ // issue arises for the other policy.
+ if (@available(iOS 18.2, *)) {
+ // 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];
+ }
+ }
+
break;
case FlutterPlatformViewGestureRecognizersBlockingPolicyWaitUntilTouchesEnded:
if (self.delayingRecognizer.touchedEndedWithoutBlocking) {
diff --git a/shell/platform/darwin/ios/framework/Source/FlutterPlatformViewsTest.mm b/shell/platform/darwin/ios/framework/Source/FlutterPlatformViewsTest.mm
index 2a890cc..2402fec 100644
--- a/shell/platform/darwin/ios/framework/Source/FlutterPlatformViewsTest.mm
+++ b/shell/platform/darwin/ios/framework/Source/FlutterPlatformViewsTest.mm
@@ -6,6 +6,7 @@
#import <OCMock/OCMock.h>
#import <UIKit/UIKit.h>
+#import <WebKit/WebKit.h>
#import <XCTest/XCTest.h>
#include <memory>
@@ -25,7 +26,7 @@
FLUTTER_ASSERT_ARC
@class FlutterPlatformViewsTestMockPlatformView;
-__weak static FlutterPlatformViewsTestMockPlatformView* gMockPlatformView = nil;
+__weak static UIView* gMockPlatformView = nil;
const float kFloatCompareEpsilon = 0.001;
@interface FlutterPlatformViewsTestMockPlatformView : UIView
@@ -73,6 +74,9 @@
self.viewCreated = YES;
}
+- (void)dealloc {
+ gMockPlatformView = nil;
+}
@end
@interface FlutterPlatformViewsTestMockFlutterPlatformFactory
@@ -88,6 +92,49 @@
@end
+@interface FlutterPlatformViewsTestMockWebView : NSObject <FlutterPlatformView>
+@property(nonatomic, strong) UIView* view;
+@property(nonatomic, assign) BOOL viewCreated;
+@end
+
+@implementation FlutterPlatformViewsTestMockWebView
+- (instancetype)init {
+ if (self = [super init]) {
+ _view = [[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 FlutterPlatformViewsTestMockWebViewFactory : NSObject <FlutterPlatformViewFactory>
+@end
+
+@implementation FlutterPlatformViewsTestMockWebViewFactory
+- (NSObject<FlutterPlatformView>*)createWithFrame:(CGRect)frame
+ viewIdentifier:(int64_t)viewId
+ arguments:(id _Nullable)args {
+ return [[FlutterPlatformViewsTestMockWebView alloc] init];
+}
+@end
+
@interface FlutterPlatformViewsTestNilFlutterPlatformFactory : NSObject <FlutterPlatformViewFactory>
@end
@@ -100,6 +147,93 @@
@end
+@interface FlutterPlatformViewsTestMockWrapperWebView : NSObject <FlutterPlatformView>
+@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 <FlutterPlatformViewFactory>
+@end
+
+@implementation FlutterPlatformViewsTestMockWrapperWebViewFactory
+- (NSObject<FlutterPlatformView>*)createWithFrame:(CGRect)frame
+ viewIdentifier:(int64_t)viewId
+ arguments:(id _Nullable)args {
+ return [[FlutterPlatformViewsTestMockWrapperWebView alloc] init];
+}
+@end
+
+@interface FlutterPlatformViewsTestMockNestedWrapperWebView : NSObject <FlutterPlatformView>
+@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 <FlutterPlatformViewFactory>
+@end
+
+@implementation FlutterPlatformViewsTestMockNestedWrapperWebViewFactory
+- (NSObject<FlutterPlatformView>*)createWithFrame:(CGRect)frame
+ viewIdentifier:(int64_t)viewId
+ arguments:(id _Nullable)args {
+ return [[FlutterPlatformViewsTestMockNestedWrapperWebView alloc] init];
+}
+@end
+
namespace flutter {
namespace {
class FlutterPlatformViewsTestMockPlatformViewDelegate : public PlatformView::Delegate {
@@ -3048,6 +3182,258 @@
[flutterPlatformViewsController reset];
}
+- (void)
+ testFlutterPlatformViewBlockGestureUnderEagerPolicyShouldRemoveAndAddBackDelayingRecognizerForWebView {
+ 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<flutter::PlatformViewIOS>(
+ /*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<fml::SyncSwitch>());
+
+ FlutterPlatformViewsTestMockWebViewFactory* factory =
+ [[FlutterPlatformViewsTestMockWebViewFactory alloc] init];
+ [flutterPlatformViewsController
+ registerViewFactory:factory
+ withId:@"MockWebView"
+ gestureRecognizersBlockingPolicy:FlutterPlatformViewGestureRecognizersBlockingPolicyEager];
+ FlutterResult result = ^(id result) {
+ };
+ [flutterPlatformViewsController
+ onMethodCall:[FlutterMethodCall
+ methodCallWithMethodName:@"create"
+ arguments:@{@"id" : @2, @"viewType" : @"MockWebView"}]
+ 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)
+ 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<flutter::PlatformViewIOS>(
+ /*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<fml::SyncSwitch>());
+
+ 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)
+ 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<flutter::PlatformViewIOS>(
+ /*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<fml::SyncSwitch>());
+
+ 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;
+
+ 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<flutter::PlatformViewIOS>(
+ /*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<fml::SyncSwitch>());
+
+ FlutterPlatformViewsTestMockFlutterPlatformFactory* factory =
+ [[FlutterPlatformViewsTestMockFlutterPlatformFactory alloc] init];
+ [flutterPlatformViewsController
+ registerViewFactory:factory
+ withId:@"MockFlutterPlatformView"
+ gestureRecognizersBlockingPolicy:FlutterPlatformViewGestureRecognizersBlockingPolicyEager];
+ FlutterResult result = ^(id result) {
+ };
+ [flutterPlatformViewsController
+ onMethodCall:[FlutterMethodCall methodCallWithMethodName:@"create"
+ arguments:@{
+ @"id" : @2,
+ @"viewType" : @"MockFlutterPlatformView"
+ }]
+ 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)testFlutterPlatformViewControllerSubmitFrameWithoutFlutterViewNotCrashing {
flutter::FlutterPlatformViewsTestMockPlatformViewDelegate mock_delegate;