Skip to content
This repository was archived by the owner on Feb 25, 2025. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions shell/platform/darwin/ios/BUILD.gn
Original file line number Diff line number Diff line change
Expand Up @@ -222,6 +222,7 @@ source_set("flutter_framework_source") {
"CoreMedia.framework",
"CoreVideo.framework",
"QuartzCore.framework",
"WebKit.framework",
"UIKit.framework",
]
if (flutter_runtime_mode == "profile" || flutter_runtime_mode == "debug") {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

#import <OCMock/OCMock.h>
#import <UIKit/UIKit.h>
#import <WebKit/WebKit.h>
#import <XCTest/XCTest.h>
#include "fml/synchronization/count_down_latch.h"
#include "shell/platform/darwin/ios/framework/Source/platform_views_controller.h"
Expand All @@ -20,7 +21,7 @@
FLUTTER_ASSERT_ARC

@class FlutterPlatformViewsTestMockPlatformView;
__weak static FlutterPlatformViewsTestMockPlatformView* gMockPlatformView = nil;
__weak static UIView* gMockPlatformView = nil;
const float kFloatCompareEpsilon = 0.001;

@interface FlutterPlatformViewsTestMockPlatformView : UIView
Expand Down Expand Up @@ -83,6 +84,45 @@ @implementation FlutterPlatformViewsTestMockFlutterPlatformFactory

@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;
}
@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

Expand Down Expand Up @@ -2782,6 +2822,124 @@ - (void)testFlutterPlatformViewTouchesCancelledEventAreForcedToBeCancelled {
flutterPlatformViewsController->Reset();
}

- (void)
testFlutterPlatformViewBlockGestureUnderEagerPolicyShouldRemoveAndAddBackDelayingRecognizerForWebView {
flutter::FlutterPlatformViewsTestMockPlatformViewDelegate mock_delegate;

flutter::TaskRunners runners(/*label=*/self.name.UTF8String,
/*platform=*/GetDefaultTaskRunner(),
/*raster=*/GetDefaultTaskRunner(),
/*ui=*/GetDefaultTaskRunner(),
/*io=*/GetDefaultTaskRunner());
auto flutterPlatformViewsController = std::make_shared<flutter::PlatformViewsController>();
flutterPlatformViewsController->SetTaskRunner(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, @"MockWebView", FlutterPlatformViewGestureRecognizersBlockingPolicyEager);
FlutterResult result = ^(id result) {
};
flutterPlatformViewsController->OnMethodCall(
[FlutterMethodCall methodCallWithMethodName:@"create"
arguments:@{@"id" : @2, @"viewType" : @"MockWebView"}],
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;

flutter::TaskRunners runners(/*label=*/self.name.UTF8String,
/*platform=*/GetDefaultTaskRunner(),
/*raster=*/GetDefaultTaskRunner(),
/*ui=*/GetDefaultTaskRunner(),
/*io=*/GetDefaultTaskRunner());
auto flutterPlatformViewsController = std::make_shared<flutter::PlatformViewsController>();
flutterPlatformViewsController->SetTaskRunner(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, @"MockFlutterPlatformView",
FlutterPlatformViewGestureRecognizersBlockingPolicyEager);
FlutterResult result = ^(id result) {
};
flutterPlatformViewsController->OnMethodCall(
[FlutterMethodCall
methodCallWithMethodName:@"create"
arguments:@{@"id" : @2, @"viewType" : @"MockFlutterPlatformView"}],
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;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -160,4 +160,43 @@
@property(nonatomic, readonly) BOOL flt_hasFirstResponderInViewHierarchySubtree;
@end

// This recognizer delays touch events from being dispatched to the responder chain until it failed
// recognizing a gesture.
//
// We only fail this recognizer when asked to do so by the Flutter framework (which does so by
// invoking an acceptGesture method on the platform_views channel). And this is how we allow the
// Flutter framework to delay or prevent the embedded view from getting a touch sequence.
@interface FlutterDelayingGestureRecognizer : UIGestureRecognizer <UIGestureRecognizerDelegate>

// Indicates that if the `FlutterDelayingGestureRecognizer`'s state should be set to
// `UIGestureRecognizerStateEnded` during next `touchesEnded` call.
@property(nonatomic) BOOL shouldEndInNextTouchesEnded;

// Indicates that the `FlutterDelayingGestureRecognizer`'s `touchesEnded` has been invoked without
// setting the state to `UIGestureRecognizerStateEnded`.
@property(nonatomic) BOOL touchedEndedWithoutBlocking;

@property(nonatomic, readonly) UIGestureRecognizer* forwardingRecognizer;

- (instancetype)initWithTarget:(id)target
action:(SEL)action
forwardingRecognizer:(UIGestureRecognizer*)forwardingRecognizer;
@end

// While the FlutterDelayingGestureRecognizer is preventing touches from hitting the responder chain
// the touch events are not arriving to the FlutterView (and thus not arriving to the Flutter
// framework). We use this gesture recognizer to dispatch the events directly to the FlutterView
// while during this phase.
//
// If the Flutter framework decides to dispatch events to the embedded view, we fail the
// FlutterDelayingGestureRecognizer which sends the events up the responder chain. But since the
// events are handled by the embedded view they are not delivered to the Flutter framework in this
// phase as well. So during this phase as well the ForwardingGestureRecognizer dispatched the events
// directly to the FlutterView.
@interface ForwardingGestureRecognizer : UIGestureRecognizer <UIGestureRecognizerDelegate>
- (instancetype)initWithTarget:(id)target
platformViewsController:
(fml::WeakPtr<flutter::PlatformViewsController>)platformViewsController;
@end

#endif // FLUTTER_SHELL_PLATFORM_DARWIN_IOS_FRAMEWORK_SOURCE_FLUTTERPLATFORMVIEWS_INTERNAL_H_
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -510,45 +512,6 @@ - (BOOL)flt_hasFirstResponderInViewHierarchySubtree {
}
@end

// This recognizer delays touch events from being dispatched to the responder chain until it failed
// recognizing a gesture.
//
// We only fail this recognizer when asked to do so by the Flutter framework (which does so by
// invoking an acceptGesture method on the platform_views channel). And this is how we allow the
// Flutter framework to delay or prevent the embedded view from getting a touch sequence.
@interface FlutterDelayingGestureRecognizer : UIGestureRecognizer <UIGestureRecognizerDelegate>

// Indicates that if the `FlutterDelayingGestureRecognizer`'s state should be set to
// `UIGestureRecognizerStateEnded` during next `touchesEnded` call.
@property(nonatomic) BOOL shouldEndInNextTouchesEnded;

// Indicates that the `FlutterDelayingGestureRecognizer`'s `touchesEnded` has been invoked without
// setting the state to `UIGestureRecognizerStateEnded`.
@property(nonatomic) BOOL touchedEndedWithoutBlocking;

@property(nonatomic, readonly) UIGestureRecognizer* forwardingRecognizer;

- (instancetype)initWithTarget:(id)target
action:(SEL)action
forwardingRecognizer:(UIGestureRecognizer*)forwardingRecognizer;
@end

// While the FlutterDelayingGestureRecognizer is preventing touches from hitting the responder chain
// the touch events are not arriving to the FlutterView (and thus not arriving to the Flutter
// framework). We use this gesture recognizer to dispatch the events directly to the FlutterView
// while during this phase.
//
// If the Flutter framework decides to dispatch events to the embedded view, we fail the
// FlutterDelayingGestureRecognizer which sends the events up the responder chain. But since the
// events are handled by the embedded view they are not delivered to the Flutter framework in this
// phase as well. So during this phase as well the ForwardingGestureRecognizer dispatched the events
// directly to the FlutterView.
@interface ForwardingGestureRecognizer : UIGestureRecognizer <UIGestureRecognizerDelegate>
- (instancetype)initWithTarget:(id)target
platformViewsController:
(fml::WeakPtr<flutter::PlatformViewsController>)platformViewsController;
@end

@interface FlutterTouchInterceptingView ()
@property(nonatomic, weak, readonly) UIView* embeddedView;
@property(nonatomic, readonly) FlutterDelayingGestureRecognizer* delayingRecognizer;
Expand Down Expand Up @@ -595,6 +558,22 @@ - (void)blockGesture {
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, *)) {
if ([self.embeddedView isKindOfClass:[WKWebView class]]) {
[self removeGestureRecognizer:self.delayingRecognizer];
[self addGestureRecognizer:self.delayingRecognizer];
}
}

break;
case FlutterPlatformViewGestureRecognizersBlockingPolicyWaitUntilTouchesEnded:
if (self.delayingRecognizer.touchedEndedWithoutBlocking) {
Expand Down