Skip to content

Commit 3e89cd7

Browse files
authored
[ios][platform_view]force reset forwarding recognizer state when its stuck (flutter/engine#55958)
This PR force reset forwarding recognizer state when it's stuck at failed state, by recreating the recognizer (we are not able to reset the state back to possible - it has to be reset by UIKit). This is a tricky one since pencil and finger triggers exactly the same callbacks. It turns out that when pencil is involved after finger interaction, the platform view's "forwarding" gesture recognizer is stuck at failed state. This seems to be an iOS bug, because according to [the API doc](https://developer.apple.com/documentation/uikit/uigesturerecognizerstate/uigesturerecognizerstatefailed?language=objc), it should be reset back to "possible" state: > No action message is sent and the gesture recognizer is reset to [UIGestureRecognizerStatePossible](https://developer.apple.com/documentation/uikit/uigesturerecognizerstate/uigesturerecognizerstatepossible?language=objc). However, when iPad pencil is involved, the state is not reset. I tried to KVO the state property, and wasn't able to capture the change. This means the state change very likely happened internally within the recognizer via the backing ivar of the state property. Fixes flutter#136244 *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
1 parent 57a7c7a commit 3e89cd7

2 files changed

Lines changed: 141 additions & 1 deletion

File tree

engine/src/flutter/shell/platform/darwin/ios/framework/Source/FlutterPlatformViewsTest.mm

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2782,6 +2782,111 @@ - (void)testFlutterPlatformViewTouchesCancelledEventAreForcedToBeCancelled {
27822782
flutterPlatformViewsController->Reset();
27832783
}
27842784

2785+
- (void)testFlutterPlatformViewTouchesEndedOrTouchesCancelledEventDoesNotFailTheGestureRecognizer {
2786+
flutter::FlutterPlatformViewsTestMockPlatformViewDelegate mock_delegate;
2787+
2788+
flutter::TaskRunners runners(/*label=*/self.name.UTF8String,
2789+
/*platform=*/GetDefaultTaskRunner(),
2790+
/*raster=*/GetDefaultTaskRunner(),
2791+
/*ui=*/GetDefaultTaskRunner(),
2792+
/*io=*/GetDefaultTaskRunner());
2793+
auto flutterPlatformViewsController = std::make_shared<flutter::PlatformViewsController>();
2794+
flutterPlatformViewsController->SetTaskRunner(GetDefaultTaskRunner());
2795+
auto platform_view = std::make_unique<flutter::PlatformViewIOS>(
2796+
/*delegate=*/mock_delegate,
2797+
/*rendering_api=*/mock_delegate.settings_.enable_impeller
2798+
? flutter::IOSRenderingAPI::kMetal
2799+
: flutter::IOSRenderingAPI::kSoftware,
2800+
/*platform_views_controller=*/flutterPlatformViewsController,
2801+
/*task_runners=*/runners,
2802+
/*worker_task_runner=*/nil,
2803+
/*is_gpu_disabled_jsync_switch=*/std::make_shared<fml::SyncSwitch>());
2804+
2805+
FlutterPlatformViewsTestMockFlutterPlatformFactory* factory =
2806+
[[FlutterPlatformViewsTestMockFlutterPlatformFactory alloc] init];
2807+
flutterPlatformViewsController->RegisterViewFactory(
2808+
factory, @"MockFlutterPlatformView",
2809+
FlutterPlatformViewGestureRecognizersBlockingPolicyEager);
2810+
FlutterResult result = ^(id result) {
2811+
};
2812+
flutterPlatformViewsController->OnMethodCall(
2813+
[FlutterMethodCall
2814+
methodCallWithMethodName:@"create"
2815+
arguments:@{@"id" : @2, @"viewType" : @"MockFlutterPlatformView"}],
2816+
result);
2817+
2818+
XCTAssertNotNil(gMockPlatformView);
2819+
2820+
// Find touch inteceptor view
2821+
UIView* touchInteceptorView = gMockPlatformView;
2822+
while (touchInteceptorView != nil &&
2823+
![touchInteceptorView isKindOfClass:[FlutterTouchInterceptingView class]]) {
2824+
touchInteceptorView = touchInteceptorView.superview;
2825+
}
2826+
XCTAssertNotNil(touchInteceptorView);
2827+
2828+
// Find ForwardGestureRecognizer
2829+
__block UIGestureRecognizer* forwardGestureRecognizer = nil;
2830+
for (UIGestureRecognizer* gestureRecognizer in touchInteceptorView.gestureRecognizers) {
2831+
if ([gestureRecognizer isKindOfClass:NSClassFromString(@"ForwardingGestureRecognizer")]) {
2832+
forwardGestureRecognizer = gestureRecognizer;
2833+
break;
2834+
}
2835+
}
2836+
id flutterViewContoller = OCMClassMock([FlutterViewController class]);
2837+
2838+
flutterPlatformViewsController->SetFlutterViewController(flutterViewContoller);
2839+
2840+
NSSet* touches1 = [NSSet setWithObject:@1];
2841+
id event1 = OCMClassMock([UIEvent class]);
2842+
XCTAssert(forwardGestureRecognizer.state == UIGestureRecognizerStatePossible,
2843+
@"Forwarding gesture recognizer must start with possible state.");
2844+
[forwardGestureRecognizer touchesBegan:touches1 withEvent:event1];
2845+
[forwardGestureRecognizer touchesEnded:touches1 withEvent:event1];
2846+
XCTAssert(forwardGestureRecognizer.state == UIGestureRecognizerStateFailed,
2847+
@"Forwarding gesture recognizer must end with failed state.");
2848+
2849+
XCTestExpectation* touchEndedExpectation =
2850+
[self expectationWithDescription:@"Wait for gesture recognizer's state change."];
2851+
dispatch_async(dispatch_get_main_queue(), ^{
2852+
// Re-query forward gesture recognizer since it's recreated.
2853+
for (UIGestureRecognizer* gestureRecognizer in touchInteceptorView.gestureRecognizers) {
2854+
if ([gestureRecognizer isKindOfClass:NSClassFromString(@"ForwardingGestureRecognizer")]) {
2855+
forwardGestureRecognizer = gestureRecognizer;
2856+
break;
2857+
}
2858+
}
2859+
XCTAssert(forwardGestureRecognizer.state == UIGestureRecognizerStatePossible,
2860+
@"Forwarding gesture recognizer must be reset to possible state.");
2861+
[touchEndedExpectation fulfill];
2862+
});
2863+
[self waitForExpectationsWithTimeout:30 handler:nil];
2864+
2865+
XCTAssert(forwardGestureRecognizer.state == UIGestureRecognizerStatePossible,
2866+
@"Forwarding gesture recognizer must start with possible state.");
2867+
[forwardGestureRecognizer touchesBegan:touches1 withEvent:event1];
2868+
[forwardGestureRecognizer touchesCancelled:touches1 withEvent:event1];
2869+
XCTAssert(forwardGestureRecognizer.state == UIGestureRecognizerStateFailed,
2870+
@"Forwarding gesture recognizer must end with failed state.");
2871+
XCTestExpectation* touchCancelledExpectation =
2872+
[self expectationWithDescription:@"Wait for gesture recognizer's state change."];
2873+
dispatch_async(dispatch_get_main_queue(), ^{
2874+
// Re-query forward gesture recognizer since it's recreated.
2875+
for (UIGestureRecognizer* gestureRecognizer in touchInteceptorView.gestureRecognizers) {
2876+
if ([gestureRecognizer isKindOfClass:NSClassFromString(@"ForwardingGestureRecognizer")]) {
2877+
forwardGestureRecognizer = gestureRecognizer;
2878+
break;
2879+
}
2880+
}
2881+
XCTAssert(forwardGestureRecognizer.state == UIGestureRecognizerStatePossible,
2882+
@"Forwarding gesture recognizer must be reset to possible state.");
2883+
[touchCancelledExpectation fulfill];
2884+
});
2885+
[self waitForExpectationsWithTimeout:30 handler:nil];
2886+
2887+
flutterPlatformViewsController->Reset();
2888+
}
2889+
27852890
- (void)testFlutterPlatformViewControllerSubmitFrameWithoutFlutterViewNotCrashing {
27862891
flutter::FlutterPlatformViewsTestMockPlatformViewDelegate mock_delegate;
27872892

engine/src/flutter/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews_Internal.mm

Lines changed: 36 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -526,7 +526,7 @@ @interface FlutterDelayingGestureRecognizer : UIGestureRecognizer <UIGestureReco
526526
// setting the state to `UIGestureRecognizerStateEnded`.
527527
@property(nonatomic) BOOL touchedEndedWithoutBlocking;
528528

529-
@property(nonatomic, readonly) UIGestureRecognizer* forwardingRecognizer;
529+
@property(nonatomic) UIGestureRecognizer* forwardingRecognizer;
530530

531531
- (instancetype)initWithTarget:(id)target
532532
action:(SEL)action
@@ -547,6 +547,7 @@ @interface ForwardingGestureRecognizer : UIGestureRecognizer <UIGestureRecognize
547547
- (instancetype)initWithTarget:(id)target
548548
platformViewsController:
549549
(fml::WeakPtr<flutter::PlatformViewsController>)platformViewsController;
550+
- (ForwardingGestureRecognizer*)recreateRecognizerWithTarget:(id)target;
550551
@end
551552

552553
@interface FlutterTouchInterceptingView ()
@@ -586,6 +587,20 @@ - (instancetype)initWithEmbeddedView:(UIView*)embeddedView
586587
return self;
587588
}
588589

590+
- (void)forceResetForwardingGestureRecognizerState {
591+
// When iPad pencil is involved in a finger touch gesture, the gesture is not reset to "possible"
592+
// state and is stuck on "failed" state, which causes subsequent touches to be blocked. As a
593+
// workaround, we force reset the state by recreating the forwarding gesture recognizer. See:
594+
// https://github.com/flutter/flutter/issues/136244
595+
ForwardingGestureRecognizer* oldForwardingRecognizer =
596+
(ForwardingGestureRecognizer*)self.delayingRecognizer.forwardingRecognizer;
597+
ForwardingGestureRecognizer* newForwardingRecognizer =
598+
[oldForwardingRecognizer recreateRecognizerWithTarget:self];
599+
self.delayingRecognizer.forwardingRecognizer = newForwardingRecognizer;
600+
[self removeGestureRecognizer:oldForwardingRecognizer];
601+
[self addGestureRecognizer:newForwardingRecognizer];
602+
}
603+
589604
- (void)releaseGesture {
590605
self.delayingRecognizer.state = UIGestureRecognizerStateFailed;
591606
}
@@ -715,6 +730,11 @@ - (instancetype)initWithTarget:(id)target
715730
return self;
716731
}
717732

733+
- (ForwardingGestureRecognizer*)recreateRecognizerWithTarget:(id)target {
734+
return [[ForwardingGestureRecognizer alloc] initWithTarget:target
735+
platformViewsController:std::move(_platformViewsController)];
736+
}
737+
718738
- (void)touchesBegan:(NSSet*)touches withEvent:(UIEvent*)event {
719739
FML_DCHECK(_currentTouchPointersCount >= 0);
720740
if (_currentTouchPointersCount == 0) {
@@ -741,6 +761,7 @@ - (void)touchesEnded:(NSSet*)touches withEvent:(UIEvent*)event {
741761
if (_currentTouchPointersCount == 0) {
742762
self.state = UIGestureRecognizerStateFailed;
743763
_flutterViewController.reset(nil);
764+
[self forceResetStateIfNeeded];
744765
}
745766
}
746767

@@ -755,9 +776,23 @@ - (void)touchesCancelled:(NSSet*)touches withEvent:(UIEvent*)event {
755776
if (_currentTouchPointersCount == 0) {
756777
self.state = UIGestureRecognizerStateFailed;
757778
_flutterViewController.reset(nil);
779+
[self forceResetStateIfNeeded];
758780
}
759781
}
760782

783+
- (void)forceResetStateIfNeeded {
784+
__weak ForwardingGestureRecognizer* weakSelf = self;
785+
dispatch_async(dispatch_get_main_queue(), ^{
786+
ForwardingGestureRecognizer* strongSelf = weakSelf;
787+
if (!strongSelf) {
788+
return;
789+
}
790+
if (strongSelf.state != UIGestureRecognizerStatePossible) {
791+
[(FlutterTouchInterceptingView*)strongSelf.view forceResetForwardingGestureRecognizerState];
792+
}
793+
});
794+
}
795+
761796
- (BOOL)gestureRecognizer:(UIGestureRecognizer*)gestureRecognizer
762797
shouldRecognizeSimultaneouslyWithGestureRecognizer:
763798
(UIGestureRecognizer*)otherGestureRecognizer {

0 commit comments

Comments
 (0)