diff --git a/shell/platform/darwin/ios/framework/Source/FlutterPlatformViewsTest.mm b/shell/platform/darwin/ios/framework/Source/FlutterPlatformViewsTest.mm index 904a3eace2564..16f5fa59beb21 100644 --- a/shell/platform/darwin/ios/framework/Source/FlutterPlatformViewsTest.mm +++ b/shell/platform/darwin/ios/framework/Source/FlutterPlatformViewsTest.mm @@ -2782,6 +2782,111 @@ - (void)testFlutterPlatformViewTouchesCancelledEventAreForcedToBeCancelled { flutterPlatformViewsController->Reset(); } +- (void)testFlutterPlatformViewTouchesEndedOrTouchesCancelledEventDoesNotFailTheGestureRecognizer { + flutter::FlutterPlatformViewsTestMockPlatformViewDelegate mock_delegate; + + flutter::TaskRunners runners(/*label=*/self.name.UTF8String, + /*platform=*/GetDefaultTaskRunner(), + /*raster=*/GetDefaultTaskRunner(), + /*ui=*/GetDefaultTaskRunner(), + /*io=*/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 + ? 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()); + + 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); + + // Find ForwardGestureRecognizer + __block UIGestureRecognizer* forwardGestureRecognizer = nil; + for (UIGestureRecognizer* gestureRecognizer in touchInteceptorView.gestureRecognizers) { + if ([gestureRecognizer isKindOfClass:NSClassFromString(@"ForwardingGestureRecognizer")]) { + forwardGestureRecognizer = gestureRecognizer; + break; + } + } + id flutterViewContoller = OCMClassMock([FlutterViewController class]); + + flutterPlatformViewsController->SetFlutterViewController(flutterViewContoller); + + NSSet* touches1 = [NSSet setWithObject:@1]; + id event1 = OCMClassMock([UIEvent class]); + XCTAssert(forwardGestureRecognizer.state == UIGestureRecognizerStatePossible, + @"Forwarding gesture recognizer must start with possible state."); + [forwardGestureRecognizer touchesBegan:touches1 withEvent:event1]; + [forwardGestureRecognizer touchesEnded:touches1 withEvent:event1]; + XCTAssert(forwardGestureRecognizer.state == UIGestureRecognizerStateFailed, + @"Forwarding gesture recognizer must end with failed state."); + + XCTestExpectation* touchEndedExpectation = + [self expectationWithDescription:@"Wait for gesture recognizer's state change."]; + dispatch_async(dispatch_get_main_queue(), ^{ + // Re-query forward gesture recognizer since it's recreated. + for (UIGestureRecognizer* gestureRecognizer in touchInteceptorView.gestureRecognizers) { + if ([gestureRecognizer isKindOfClass:NSClassFromString(@"ForwardingGestureRecognizer")]) { + forwardGestureRecognizer = gestureRecognizer; + break; + } + } + XCTAssert(forwardGestureRecognizer.state == UIGestureRecognizerStatePossible, + @"Forwarding gesture recognizer must be reset to possible state."); + [touchEndedExpectation fulfill]; + }); + [self waitForExpectationsWithTimeout:30 handler:nil]; + + XCTAssert(forwardGestureRecognizer.state == UIGestureRecognizerStatePossible, + @"Forwarding gesture recognizer must start with possible state."); + [forwardGestureRecognizer touchesBegan:touches1 withEvent:event1]; + [forwardGestureRecognizer touchesCancelled:touches1 withEvent:event1]; + XCTAssert(forwardGestureRecognizer.state == UIGestureRecognizerStateFailed, + @"Forwarding gesture recognizer must end with failed state."); + XCTestExpectation* touchCancelledExpectation = + [self expectationWithDescription:@"Wait for gesture recognizer's state change."]; + dispatch_async(dispatch_get_main_queue(), ^{ + // Re-query forward gesture recognizer since it's recreated. + for (UIGestureRecognizer* gestureRecognizer in touchInteceptorView.gestureRecognizers) { + if ([gestureRecognizer isKindOfClass:NSClassFromString(@"ForwardingGestureRecognizer")]) { + forwardGestureRecognizer = gestureRecognizer; + break; + } + } + XCTAssert(forwardGestureRecognizer.state == UIGestureRecognizerStatePossible, + @"Forwarding gesture recognizer must be reset to possible state."); + [touchCancelledExpectation fulfill]; + }); + [self waitForExpectationsWithTimeout:30 handler:nil]; + + flutterPlatformViewsController->Reset(); +} + - (void)testFlutterPlatformViewControllerSubmitFrameWithoutFlutterViewNotCrashing { 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 5e76654bed179..d249d44e760c9 100644 --- a/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews_Internal.mm +++ b/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews_Internal.mm @@ -526,7 +526,7 @@ @interface FlutterDelayingGestureRecognizer : UIGestureRecognizer )platformViewsController; +- (ForwardingGestureRecognizer*)recreateRecognizerWithTarget:(id)target; @end @interface FlutterTouchInterceptingView () @@ -586,6 +587,20 @@ - (instancetype)initWithEmbeddedView:(UIView*)embeddedView return self; } +- (void)forceResetForwardingGestureRecognizerState { + // When iPad pencil is involved in a finger touch gesture, the gesture is not reset to "possible" + // state and is stuck on "failed" state, which causes subsequent touches to be blocked. As a + // workaround, we force reset the state by recreating the forwarding gesture recognizer. See: + // https://github.com/flutter/flutter/issues/136244 + ForwardingGestureRecognizer* oldForwardingRecognizer = + (ForwardingGestureRecognizer*)self.delayingRecognizer.forwardingRecognizer; + ForwardingGestureRecognizer* newForwardingRecognizer = + [oldForwardingRecognizer recreateRecognizerWithTarget:self]; + self.delayingRecognizer.forwardingRecognizer = newForwardingRecognizer; + [self removeGestureRecognizer:oldForwardingRecognizer]; + [self addGestureRecognizer:newForwardingRecognizer]; +} + - (void)releaseGesture { self.delayingRecognizer.state = UIGestureRecognizerStateFailed; } @@ -715,6 +730,11 @@ - (instancetype)initWithTarget:(id)target return self; } +- (ForwardingGestureRecognizer*)recreateRecognizerWithTarget:(id)target { + return [[ForwardingGestureRecognizer alloc] initWithTarget:target + platformViewsController:std::move(_platformViewsController)]; +} + - (void)touchesBegan:(NSSet*)touches withEvent:(UIEvent*)event { FML_DCHECK(_currentTouchPointersCount >= 0); if (_currentTouchPointersCount == 0) { @@ -741,6 +761,7 @@ - (void)touchesEnded:(NSSet*)touches withEvent:(UIEvent*)event { if (_currentTouchPointersCount == 0) { self.state = UIGestureRecognizerStateFailed; _flutterViewController.reset(nil); + [self forceResetStateIfNeeded]; } } @@ -755,9 +776,23 @@ - (void)touchesCancelled:(NSSet*)touches withEvent:(UIEvent*)event { if (_currentTouchPointersCount == 0) { self.state = UIGestureRecognizerStateFailed; _flutterViewController.reset(nil); + [self forceResetStateIfNeeded]; } } +- (void)forceResetStateIfNeeded { + __weak ForwardingGestureRecognizer* weakSelf = self; + dispatch_async(dispatch_get_main_queue(), ^{ + ForwardingGestureRecognizer* strongSelf = weakSelf; + if (!strongSelf) { + return; + } + if (strongSelf.state != UIGestureRecognizerStatePossible) { + [(FlutterTouchInterceptingView*)strongSelf.view forceResetForwardingGestureRecognizerState]; + } + }); +} + - (BOOL)gestureRecognizer:(UIGestureRecognizer*)gestureRecognizer shouldRecognizeSimultaneouslyWithGestureRecognizer: (UIGestureRecognizer*)otherGestureRecognizer {