diff --git a/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews.mm b/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews.mm index f65cb5d5dd7..fb837ec92ef 100644 --- a/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews.mm +++ b/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews.mm @@ -15,6 +15,7 @@ #import "flutter/shell/platform/darwin/common/framework/Headers/FlutterChannels.h" #import "flutter/shell/platform/darwin/ios/framework/Source/FlutterOverlayView.h" #import "flutter/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews_Internal.h" +#import "flutter/shell/platform/darwin/ios/framework/Source/FlutterViewController_Internal.h" #import "flutter/shell/platform/darwin/ios/ios_surface.h" #import "flutter/shell/platform/darwin/ios/ios_surface_gl.h" @@ -938,7 +939,12 @@ void FlutterPlatformViewsController::ResetFrameState() { } - (void)touchesCancelled:(NSSet*)touches withEvent:(UIEvent*)event { - [_flutterViewController.get() touchesCancelled:touches withEvent:event]; + // In the event of platform view is removed, iOS generates a "stationary" change type instead of + // "cancelled" change type. + // Flutter needs all the cancelled touches to be "cancelled" change types in order to correctly + // handle gesture sequence. + // We always override the change type to "cancelled". + [((FlutterViewController*)_flutterViewController.get()) forceTouchesCancelled:touches]; _currentTouchPointersCount -= touches.count; if (_currentTouchPointersCount == 0) { self.state = UIGestureRecognizerStateFailed; diff --git a/shell/platform/darwin/ios/framework/Source/FlutterPlatformViewsTest.mm b/shell/platform/darwin/ios/framework/Source/FlutterPlatformViewsTest.mm index c826a2e9723..a9d6dbd9d84 100644 --- a/shell/platform/darwin/ios/framework/Source/FlutterPlatformViewsTest.mm +++ b/shell/platform/darwin/ios/framework/Source/FlutterPlatformViewsTest.mm @@ -626,7 +626,7 @@ fml::RefPtr CreateNewThread(std::string name) { // Before setting flutter view controller, events are not dispatched. NSSet* touches1 = [[[NSSet alloc] init] autorelease]; id event1 = OCMClassMock([UIEvent class]); - id mockFlutterViewContoller = OCMClassMock([UIViewController class]); + id mockFlutterViewContoller = OCMClassMock([FlutterViewController class]); [forwardGectureRecognizer touchesBegan:touches1 withEvent:event1]; OCMReject([mockFlutterViewContoller touchesBegan:touches1 withEvent:event1]); @@ -684,7 +684,7 @@ fml::RefPtr CreateNewThread(std::string name) { break; } } - id mockFlutterViewContoller = OCMClassMock([UIViewController class]); + id mockFlutterViewContoller = OCMClassMock([FlutterViewController class]); { // ***** Sequence 1, finishing touch event with touchEnded ***** // flutterPlatformViewsController->SetFlutterViewController(mockFlutterViewContoller); @@ -739,7 +739,7 @@ fml::RefPtr CreateNewThread(std::string name) { NSSet* touches3 = [[[NSSet alloc] init] autorelease]; id event3 = OCMClassMock([UIEvent class]); [forwardGectureRecognizer touchesCancelled:touches3 withEvent:event3]; - OCMVerify([mockFlutterViewContoller touchesCancelled:touches3 withEvent:event3]); + OCMVerify([mockFlutterViewContoller forceTouchesCancelled:touches3]); // Now the 2nd touch sequence should not be allowed. NSSet* touches4 = [[[NSSet alloc] init] autorelease]; @@ -803,7 +803,7 @@ fml::RefPtr CreateNewThread(std::string name) { break; } } - id mockFlutterViewContoller = OCMClassMock([UIViewController class]); + id mockFlutterViewContoller = OCMClassMock([FlutterViewController class]); flutterPlatformViewsController->SetFlutterViewController(mockFlutterViewContoller); @@ -866,6 +866,66 @@ fml::RefPtr CreateNewThread(std::string name) { flutterPlatformViewsController->Reset(); } +- (void)testFlutterPlatformViewTouchesCancelledEventAreForcedToBeCancelled { + 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; + } + } + id mockFlutterViewContoller = OCMClassMock([FlutterViewController class]); + + flutterPlatformViewsController->SetFlutterViewController(mockFlutterViewContoller); + + NSSet* touches1 = [NSSet setWithObject:@1]; + id event1 = OCMClassMock([UIEvent class]); + [forwardGectureRecognizer touchesBegan:touches1 withEvent:event1]; + + [forwardGectureRecognizer touchesCancelled:touches1 withEvent:event1]; + OCMVerify([mockFlutterViewContoller forceTouchesCancelled:touches1]); + + flutterPlatformViewsController->Reset(); +} + - (void)testFlutterPlatformViewControllerSubmitFrameWithoutFlutterViewNotCrashing { flutter::FlutterPlatformViewsTestMockPlatformViewDelegate mock_delegate; auto thread_task_runner = CreateNewThread("FlutterPlatformViewsTest"); diff --git a/shell/platform/darwin/ios/framework/Source/FlutterViewController.mm b/shell/platform/darwin/ios/framework/Source/FlutterViewController.mm index 42eb02efdfa..91c73b0b3bc 100644 --- a/shell/platform/darwin/ios/framework/Source/FlutterViewController.mm +++ b/shell/platform/darwin/ios/framework/Source/FlutterViewController.mm @@ -947,6 +947,11 @@ static flutter::PointerData::DeviceKind DeviceKindFromTouchType(UITouch* touch) [self dispatchTouches:touches pointerDataChangeOverride:nullptr]; } +- (void)forceTouchesCancelled:(NSSet*)touches { + flutter::PointerData::Change cancel = flutter::PointerData::Change::kCancel; + [self dispatchTouches:touches pointerDataChangeOverride:&cancel]; +} + #pragma mark - Handle view resizing - (void)updateViewportMetrics { diff --git a/shell/platform/darwin/ios/framework/Source/FlutterViewController_Internal.h b/shell/platform/darwin/ios/framework/Source/FlutterViewController_Internal.h index 6444a2c6e12..80bad5ed20f 100644 --- a/shell/platform/darwin/ios/framework/Source/FlutterViewController_Internal.h +++ b/shell/platform/darwin/ios/framework/Source/FlutterViewController_Internal.h @@ -29,6 +29,9 @@ extern NSNotificationName const FlutterViewControllerShowHomeIndicator; - (fml::WeakPtr)getWeakPtr; - (std::shared_ptr&)platformViewsController; - (FlutterRestorationPlugin*)restorationPlugin; +// Send touches to the Flutter Engine while forcing the change type to be cancelled. +// The `phase`s in `touches` are ignored. +- (void)forceTouchesCancelled:(NSSet*)touches; @end