diff --git a/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews.mm b/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews.mm index 989019d80d7c7..3fc52d9dd5f5b 100644 --- a/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews.mm +++ b/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews.mm @@ -871,6 +871,11 @@ @implementation ForwardingGestureRecognizer { fml::WeakPtr _platformViewsController; // Counting the pointers that has started in one touch sequence. NSInteger _currentTouchPointersCount; + // We can't dispatch events to the framework without this back pointer. + // This gesture recognizer retains the `FlutterViewController` until the + // end of a gesture sequence, that is all the touches in touchesBegan are concluded + // with |touchesCancelled| or |touchesEnded|. + fml::scoped_nsobject _flutterViewController; } - (instancetype)initWithTarget:(id)target @@ -887,16 +892,23 @@ - (instancetype)initWithTarget:(id)target } - (void)touchesBegan:(NSSet*)touches withEvent:(UIEvent*)event { - [_platformViewsController->getFlutterViewController() touchesBegan:touches withEvent:event]; + FML_DCHECK(_currentTouchPointersCount >= 0); + if (_currentTouchPointersCount == 0) { + // At the start of each gesture sequence, we reset the `_flutterViewController`, + // so that all the touch events in the same sequence are forwarded to the same + // `_flutterViewController`. + _flutterViewController.reset([_platformViewsController->getFlutterViewController() retain]); + } + [_flutterViewController.get() touchesBegan:touches withEvent:event]; _currentTouchPointersCount += touches.count; } - (void)touchesMoved:(NSSet*)touches withEvent:(UIEvent*)event { - [_platformViewsController->getFlutterViewController() touchesMoved:touches withEvent:event]; + [_flutterViewController.get() touchesMoved:touches withEvent:event]; } - (void)touchesEnded:(NSSet*)touches withEvent:(UIEvent*)event { - [_platformViewsController->getFlutterViewController() touchesEnded:touches withEvent:event]; + [_flutterViewController.get() touchesEnded:touches withEvent:event]; _currentTouchPointersCount -= touches.count; // Touches in one touch sequence are sent to the touchesEnded method separately if different // fingers stop touching the screen at different time. So one touchesEnded method triggering does @@ -904,13 +916,17 @@ - (void)touchesEnded:(NSSet*)touches withEvent:(UIEvent*)event { // UIGestureRecognizerStateFailed when all the touches in the current touch sequence is ended. if (_currentTouchPointersCount == 0) { self.state = UIGestureRecognizerStateFailed; + _flutterViewController.reset(nil); } } - (void)touchesCancelled:(NSSet*)touches withEvent:(UIEvent*)event { - [_platformViewsController->getFlutterViewController() touchesCancelled:touches withEvent:event]; - _currentTouchPointersCount = 0; - self.state = UIGestureRecognizerStateFailed; + [_flutterViewController.get() touchesCancelled:touches withEvent:event]; + _currentTouchPointersCount -= touches.count; + if (_currentTouchPointersCount == 0) { + self.state = UIGestureRecognizerStateFailed; + _flutterViewController.reset(nil); + } } - (BOOL)gestureRecognizer:(UIGestureRecognizer*)gestureRecognizer diff --git a/shell/platform/darwin/ios/framework/Source/FlutterPlatformViewsTest.mm b/shell/platform/darwin/ios/framework/Source/FlutterPlatformViewsTest.mm index 988fb0f5c711d..0334720337049 100644 --- a/shell/platform/darwin/ios/framework/Source/FlutterPlatformViewsTest.mm +++ b/shell/platform/darwin/ios/framework/Source/FlutterPlatformViewsTest.mm @@ -568,6 +568,196 @@ - (void)testSetFlutterViewControllerAfterCreateCanStillDispatchTouchEvents { flutterPlatformViewsController->Reset(); } +- (void)testSetFlutterViewControllerInTheMiddleOfTouchEventShouldStillAllowGesturesToBeHandled { + flutter::FlutterPlatformViewsTestMockPlatformViewDelegate mock_delegate; + auto thread_task_runner = CreateNewThread("FlutterPlatformViewsTest"); + flutter::TaskRunners runners(/*label=*/self.name.UTF8String, + /*platform=*/thread_task_runner, + /*raster=*/thread_task_runner, + /*ui=*/thread_task_runner, + /*io=*/thread_task_runner); + auto flutterPlatformViewsController = std::make_shared(); + auto platform_view = std::make_unique( + /*delegate=*/mock_delegate, + /*rendering_api=*/flutter::IOSRenderingAPI::kSoftware, + /*platform_views_controller=*/flutterPlatformViewsController, + /*task_runners=*/runners); + + FlutterPlatformViewsTestMockFlutterPlatformFactory* factory = + [[FlutterPlatformViewsTestMockFlutterPlatformFactory new] autorelease]; + 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 + UIGestureRecognizer* forwardGectureRecognizer = nil; + for (UIGestureRecognizer* gestureRecognizer in touchInteceptorView.gestureRecognizers) { + if ([gestureRecognizer isKindOfClass:NSClassFromString(@"ForwardingGestureRecognizer")]) { + forwardGectureRecognizer = gestureRecognizer; + break; + } + } + UIViewController* mockFlutterViewContoller = OCMClassMock([UIViewController class]); + { + // ***** Sequence 1, finishing touch event with touchEnded ***** // + + flutterPlatformViewsController->SetFlutterViewController(mockFlutterViewContoller); + + NSSet* touches1 = OCMClassMock([NSSet class]); + UIEvent* event1 = OCMClassMock([UIEvent class]); + [forwardGectureRecognizer touchesBegan:touches1 withEvent:event1]; + OCMVerify([mockFlutterViewContoller touchesBegan:touches1 withEvent:event1]); + + flutterPlatformViewsController->SetFlutterViewController(nil); + + // Allow the touch events to finish + NSSet* touches2 = OCMClassMock([NSSet class]); + UIEvent* event2 = OCMClassMock([UIEvent class]); + [forwardGectureRecognizer touchesMoved:touches2 withEvent:event2]; + OCMVerify([mockFlutterViewContoller touchesMoved:touches2 withEvent:event2]); + + NSSet* touches3 = OCMClassMock([NSSet class]); + UIEvent* event3 = OCMClassMock([UIEvent class]); + [forwardGectureRecognizer touchesEnded:touches3 withEvent:event3]; + OCMVerify([mockFlutterViewContoller touchesEnded:touches3 withEvent:event3]); + + // Now the 2nd touch sequence should not be allowed. + NSSet* touches4 = OCMClassMock([NSSet class]); + UIEvent* event4 = OCMClassMock([UIEvent class]); + mockFlutterViewContoller = OCMClassMock([UIViewController class]); + [forwardGectureRecognizer touchesBegan:touches4 withEvent:event4]; + OCMReject([mockFlutterViewContoller touchesBegan:touches4 withEvent:event4]); + + NSSet* touches5 = OCMClassMock([NSSet class]); + UIEvent* event5 = OCMClassMock([UIEvent class]); + [forwardGectureRecognizer touchesEnded:touches5 withEvent:event5]; + OCMReject([mockFlutterViewContoller touchesEnded:touches5 withEvent:event5]); + } + + { + // ***** Sequence 2, finishing touch event with touchCancelled ***** // + flutterPlatformViewsController->SetFlutterViewController(mockFlutterViewContoller); + + NSSet* touches1 = OCMClassMock([NSSet class]); + UIEvent* event1 = OCMClassMock([UIEvent class]); + [forwardGectureRecognizer touchesBegan:touches1 withEvent:event1]; + OCMVerify([mockFlutterViewContoller touchesBegan:touches1 withEvent:event1]); + + flutterPlatformViewsController->SetFlutterViewController(nil); + + // Allow the touch events to finish + NSSet* touches2 = OCMClassMock([NSSet class]); + UIEvent* event2 = OCMClassMock([UIEvent class]); + [forwardGectureRecognizer touchesMoved:touches2 withEvent:event2]; + OCMVerify([mockFlutterViewContoller touchesMoved:touches2 withEvent:event2]); + + NSSet* touches3 = OCMClassMock([NSSet class]); + UIEvent* event3 = OCMClassMock([UIEvent class]); + [forwardGectureRecognizer touchesCancelled:touches3 withEvent:event3]; + OCMVerify([mockFlutterViewContoller touchesCancelled:touches3 withEvent:event3]); + + // Now the 2nd touch sequence should not be allowed. + NSSet* touches4 = OCMClassMock([NSSet class]); + UIEvent* event4 = OCMClassMock([UIEvent class]); + mockFlutterViewContoller = OCMClassMock([UIViewController class]); + [forwardGectureRecognizer touchesBegan:touches4 withEvent:event4]; + OCMReject([mockFlutterViewContoller touchesBegan:touches4 withEvent:event4]); + + NSSet* touches5 = OCMClassMock([NSSet class]); + UIEvent* event5 = OCMClassMock([UIEvent class]); + [forwardGectureRecognizer touchesEnded:touches5 withEvent:event5]; + OCMReject([mockFlutterViewContoller touchesEnded:touches5 withEvent:event5]); + } + + { + // ***** Sequence 3, multile touches in one sequence with setting flutter view controllers in + // between ***** // + flutterPlatformViewsController->SetFlutterViewController(mockFlutterViewContoller); + + NSSet* touches1 = OCMClassMock([NSSet class]); + OCMStub([touches1 count]).andReturn(1); + UIEvent* event1 = OCMClassMock([UIEvent class]); + [forwardGectureRecognizer touchesBegan:touches1 withEvent:event1]; + OCMVerify([mockFlutterViewContoller touchesBegan:touches1 withEvent:event1]); + + UIViewController* mockFlutterViewContoller2 = OCMClassMock([UIViewController class]); + flutterPlatformViewsController->SetFlutterViewController(mockFlutterViewContoller2); + + // Touch events should still send to the old FlutterViewController if FlutterViewController + // is updated in between. + NSSet* touches2 = OCMClassMock([NSSet class]); + OCMStub([touches2 count]).andReturn(1); + UIEvent* event2 = OCMClassMock([UIEvent class]); + [forwardGectureRecognizer touchesBegan:touches2 withEvent:event2]; + OCMVerify([mockFlutterViewContoller touchesBegan:touches2 withEvent:event2]); + OCMReject([mockFlutterViewContoller2 touchesBegan:touches2 withEvent:event2]); + + NSSet* touches3 = OCMClassMock([NSSet class]); + OCMStub([touches3 count]).andReturn(1); + UIEvent* event3 = OCMClassMock([UIEvent class]); + [forwardGectureRecognizer touchesMoved:touches3 withEvent:event3]; + OCMVerify([mockFlutterViewContoller touchesMoved:touches3 withEvent:event3]); + OCMReject([mockFlutterViewContoller2 touchesMoved:touches3 withEvent:event3]); + + NSSet* touches4 = OCMClassMock([NSSet class]); + OCMStub([touches4 count]).andReturn(1); + UIEvent* event4 = OCMClassMock([UIEvent class]); + [forwardGectureRecognizer touchesEnded:touches4 withEvent:event4]; + OCMVerify([mockFlutterViewContoller touchesEnded:touches4 withEvent:event4]); + OCMReject([mockFlutterViewContoller2 touchesEnded:touches4 withEvent:event4]); + + NSSet* touches5 = OCMClassMock([NSSet class]); + OCMStub([touches5 count]).andReturn(1); + UIEvent* event5 = OCMClassMock([UIEvent class]); + [forwardGectureRecognizer touchesEnded:touches5 withEvent:event5]; + OCMVerify([mockFlutterViewContoller touchesEnded:touches5 withEvent:event5]); + OCMReject([mockFlutterViewContoller2 touchesEnded:touches5 withEvent:event5]); + + // Now the 2nd touch sequence should go to the new FlutterViewController + + NSSet* touches6 = OCMClassMock([NSSet class]); + OCMStub([touches6 count]).andReturn(1); + UIEvent* event6 = OCMClassMock([UIEvent class]); + [forwardGectureRecognizer touchesBegan:touches6 withEvent:event6]; + OCMVerify([mockFlutterViewContoller2 touchesBegan:touches6 withEvent:event6]); + OCMReject([mockFlutterViewContoller touchesBegan:touches6 withEvent:event6]); + + // Allow the touch events to finish + NSSet* touches7 = OCMClassMock([NSSet class]); + OCMStub([touches7 count]).andReturn(1); + UIEvent* event7 = OCMClassMock([UIEvent class]); + [forwardGectureRecognizer touchesMoved:touches7 withEvent:event7]; + OCMVerify([mockFlutterViewContoller2 touchesMoved:touches7 withEvent:event7]); + OCMReject([mockFlutterViewContoller touchesMoved:touches7 withEvent:event7]); + + NSSet* touches8 = OCMClassMock([NSSet class]); + OCMStub([touches8 count]).andReturn(1); + UIEvent* event8 = OCMClassMock([UIEvent class]); + [forwardGectureRecognizer touchesEnded:touches8 withEvent:event8]; + OCMVerify([mockFlutterViewContoller2 touchesEnded:touches8 withEvent:event8]); + OCMReject([mockFlutterViewContoller touchesEnded:touches8 withEvent:event8]); + } + + flutterPlatformViewsController->Reset(); +} + - (void)testFlutterPlatformViewControllerSubmitFrameWithoutFlutterViewNotCrashing { flutter::FlutterPlatformViewsTestMockPlatformViewDelegate mock_delegate; auto thread_task_runner = CreateNewThread("FlutterPlatformViewsTest");