diff --git a/engine/src/flutter/shell/platform/darwin/ios/BUILD.gn b/engine/src/flutter/shell/platform/darwin/ios/BUILD.gn index b64c400d42b..3abf9f464da 100644 --- a/engine/src/flutter/shell/platform/darwin/ios/BUILD.gn +++ b/engine/src/flutter/shell/platform/darwin/ios/BUILD.gn @@ -111,6 +111,8 @@ source_set("flutter_framework_source") { "framework/Source/FlutterRestorationPlugin.h", "framework/Source/FlutterRestorationPlugin.mm", "framework/Source/FlutterSceneDelegate.m", + "framework/Source/FlutterSceneLifecycle.h", + "framework/Source/FlutterSceneLifecycle.mm", "framework/Source/FlutterSemanticsScrollView.h", "framework/Source/FlutterSemanticsScrollView.mm", "framework/Source/FlutterSharedApplication.h", @@ -262,6 +264,7 @@ if (enable_ios_unittests) { "framework/Source/FlutterPluginAppLifeCycleDelegateTest.mm", "framework/Source/FlutterRestorationPluginTest.mm", "framework/Source/FlutterSceneDelegateTest.m", + "framework/Source/FlutterSceneLifecycleTest.mm", "framework/Source/FlutterSharedApplicationTest.mm", "framework/Source/FlutterSpellCheckPluginTest.mm", "framework/Source/FlutterTextInputPluginTest.mm", diff --git a/engine/src/flutter/shell/platform/darwin/ios/framework/Source/FlutterSceneDelegate.m b/engine/src/flutter/shell/platform/darwin/ios/framework/Source/FlutterSceneDelegate.m index b73f764591b..25e6f214043 100644 --- a/engine/src/flutter/shell/platform/darwin/ios/framework/Source/FlutterSceneDelegate.m +++ b/engine/src/flutter/shell/platform/darwin/ios/framework/Source/FlutterSceneDelegate.m @@ -6,12 +6,25 @@ #import "flutter/shell/platform/darwin/common/framework/Headers/FlutterMacros.h" #import "flutter/shell/platform/darwin/ios/framework/Source/FlutterAppDelegate_Internal.h" +#import "flutter/shell/platform/darwin/ios/framework/Source/FlutterSceneLifecycle.h" #import "flutter/shell/platform/darwin/ios/framework/Source/FlutterSharedApplication.h" FLUTTER_ASSERT_ARC +@interface FlutterSceneDelegate () +@end + @implementation FlutterSceneDelegate +@synthesize sceneLifeCycleDelegate = _sceneLifeCycleDelegate; + +- (instancetype)init { + if (self = [super init]) { + _sceneLifeCycleDelegate = [[FlutterPluginSceneLifeCycleDelegate alloc] init]; + } + return self; +} + - (void)scene:(UIScene*)scene willConnectToSession:(UISceneSession*)session options:(UISceneConnectionOptions*)connectionOptions { diff --git a/engine/src/flutter/shell/platform/darwin/ios/framework/Source/FlutterSceneLifecycle.h b/engine/src/flutter/shell/platform/darwin/ios/framework/Source/FlutterSceneLifecycle.h new file mode 100644 index 00000000000..90fc54ddc31 --- /dev/null +++ b/engine/src/flutter/shell/platform/darwin/ios/framework/Source/FlutterSceneLifecycle.h @@ -0,0 +1,29 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef FLUTTER_SHELL_PLATFORM_DARWIN_IOS_FRAMEWORK_SOURCE_FLUTTERSCENELIFECYCLE_H_ +#define FLUTTER_SHELL_PLATFORM_DARWIN_IOS_FRAMEWORK_SOURCE_FLUTTERSCENELIFECYCLE_H_ + +#import "flutter/shell/platform/darwin/ios/framework/Headers/FlutterViewController.h" + +/** + * Propagates `UIWindowSceneDelegate` callbacks to the `FlutterEngine` to then be propograted to + * registered plugins. + */ +@interface FlutterPluginSceneLifeCycleDelegate : NSObject + +- (void)addFlutterEngine:(FlutterEngine*)engine; + +- (void)removeFlutterEngine:(FlutterEngine*)engine; +@end + +/** + * Implement this in the `UIWindowSceneDelegate` of your app to enable Flutter plugins to register + * themselves to the scene life cycle events. + */ +@protocol FlutterSceneLifeCycleProvider +@property(nonatomic, strong) FlutterPluginSceneLifeCycleDelegate* sceneLifeCycleDelegate; +@end + +#endif // FLUTTER_SHELL_PLATFORM_DARWIN_IOS_FRAMEWORK_SOURCE_FLUTTERSCENELIFECYCLE_H_ diff --git a/engine/src/flutter/shell/platform/darwin/ios/framework/Source/FlutterSceneLifecycle.mm b/engine/src/flutter/shell/platform/darwin/ios/framework/Source/FlutterSceneLifecycle.mm new file mode 100644 index 00000000000..25441d123f0 --- /dev/null +++ b/engine/src/flutter/shell/platform/darwin/ios/framework/Source/FlutterSceneLifecycle.mm @@ -0,0 +1,89 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#import "flutter/shell/platform/darwin/ios/framework/Source/FlutterSceneLifecycle.h" +#import "flutter/shell/platform/darwin/common/framework/Headers/FlutterMacros.h" + +FLUTTER_ASSERT_ARC + +@interface FlutterPluginSceneLifeCycleDelegate () + +/** + * An array of weak pointers to `FlutterEngine`s that have views within this scene. + * + * This array is lazily cleaned up. `updateEnginesInScene:` should be called before use to ensure it + * is up-to-date. + */ +@property(nonatomic, strong) NSPointerArray* engines; +@end + +@implementation FlutterPluginSceneLifeCycleDelegate +- (instancetype)init { + if (self = [super init]) { + _engines = [NSPointerArray weakObjectsPointerArray]; + } + return self; +} + +- (void)addFlutterEngine:(FlutterEngine*)engine { + // Check if the engine is already in the array to avoid duplicates. + if ([self.engines.allObjects containsObject:engine]) { + return; + } + + [self.engines addPointer:(__bridge void*)engine]; + + // NSPointerArray is clever and assumes that unless a mutation operation has occurred on it that + // has set one of its values to nil, nothing could have changed and it can skip compaction. + // That's reasonable behaviour on a regular NSPointerArray but not for a weakObjectPointerArray. + // As a workaround, we mutate it first. See: http://www.openradar.me/15396578 + [self.engines addPointer:nil]; + [self.engines compact]; +} + +- (void)removeFlutterEngine:(FlutterEngine*)engine { + NSUInteger index = [self.engines.allObjects indexOfObject:engine]; + if (index != NSNotFound) { + [self.engines removePointerAtIndex:index]; + } +} + +- (void)updateEnginesInScene:(UIScene*)scene { + // Removes engines that are no longer in the scene or have been deallocated. + // + // This also handles the case where a FlutterEngine's view has been moved to a different scene. + for (NSUInteger i = 0; i < self.engines.count; i++) { + FlutterEngine* engine = (FlutterEngine*)[self.engines pointerAtIndex:i]; + + // The engine may be nil if it has been deallocated. + if (engine == nil) { + [self.engines removePointerAtIndex:i]; + i--; + continue; + } + + // There aren't any events that inform us when a UIWindow changes scenes. + // If a developer moves an entire UIWindow to a different scene and that window has a + // FlutterView inside of it, its engine will still be in its original scene's + // FlutterPluginSceneLifeCycleDelegate. The best we can do is move the engine to the correct + // scene here. Due to this, when moving a UIWindow from one scene to another, its first scene + // event may be lost. Since Flutter does not fully support multi-scene and this is an edge + // case, this is a loss we can deal with. To workaround this, the developer can move the + // UIView instead of the UIWindow, which will use willMoveToWindow to add/remove the engine from + // the scene. + UIWindowScene* engineScene = engine.viewController.view.window.windowScene; + if (engineScene != nil && engineScene != scene) { + [self.engines removePointerAtIndex:i]; + i--; + + if ([engineScene.delegate conformsToProtocol:@protocol(FlutterSceneLifeCycleProvider)]) { + id lifeCycleProvider = + (id)engineScene.delegate; + [lifeCycleProvider.sceneLifeCycleDelegate addFlutterEngine:engine]; + } + continue; + } + } +} +@end diff --git a/engine/src/flutter/shell/platform/darwin/ios/framework/Source/FlutterSceneLifecycleTest.mm b/engine/src/flutter/shell/platform/darwin/ios/framework/Source/FlutterSceneLifecycleTest.mm new file mode 100644 index 00000000000..54db970f183 --- /dev/null +++ b/engine/src/flutter/shell/platform/darwin/ios/framework/Source/FlutterSceneLifecycleTest.mm @@ -0,0 +1,137 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#import +#import +#import + +#import "flutter/shell/platform/darwin/ios/framework/Source/FlutterSceneLifecycle.h" +#import "flutter/shell/platform/darwin/ios/framework/Source/FlutterSceneLifecycle_Test.h" + +FLUTTER_ASSERT_ARC + +@interface FlutterSceneLifecycleTest : XCTestCase +@end + +@implementation FlutterSceneLifecycleTest +- (void)setUp { +} + +- (void)tearDown { +} + +- (void)testAddFlutterEngine { + FlutterPluginSceneLifeCycleDelegate* delegate = + [[FlutterPluginSceneLifeCycleDelegate alloc] init]; + + id mockEngine = OCMClassMock([FlutterEngine class]); + [delegate addFlutterEngine:mockEngine]; + XCTAssertEqual(delegate.engines.count, 1.0); +} + +- (void)testAddDuplicateFlutterEngine { + FlutterPluginSceneLifeCycleDelegate* delegate = + [[FlutterPluginSceneLifeCycleDelegate alloc] init]; + + id mockEngine = OCMClassMock([FlutterEngine class]); + [delegate addFlutterEngine:mockEngine]; + [delegate addFlutterEngine:mockEngine]; + XCTAssertEqual(delegate.engines.count, 1.0); +} + +- (void)testAddMultipleFlutterEngine { + FlutterPluginSceneLifeCycleDelegate* delegate = + [[FlutterPluginSceneLifeCycleDelegate alloc] init]; + + id mockEngine = OCMClassMock([FlutterEngine class]); + [delegate addFlutterEngine:mockEngine]; + + id mockEngine2 = OCMClassMock([FlutterEngine class]); + [delegate addFlutterEngine:mockEngine2]; + + XCTAssertEqual(delegate.engines.count, 2.0); +} + +- (void)testRemoveFlutterEngine { + FlutterPluginSceneLifeCycleDelegate* delegate = + [[FlutterPluginSceneLifeCycleDelegate alloc] init]; + + id mockEngine = OCMClassMock([FlutterEngine class]); + [delegate addFlutterEngine:mockEngine]; + XCTAssertEqual(delegate.engines.count, 1.0); + + [delegate removeFlutterEngine:mockEngine]; + XCTAssertEqual(delegate.engines.count, 0.0); +} + +- (void)testRemoveNotFoundFlutterEngine { + FlutterPluginSceneLifeCycleDelegate* delegate = + [[FlutterPluginSceneLifeCycleDelegate alloc] init]; + + id mockEngine = OCMClassMock([FlutterEngine class]); + XCTAssertEqual(delegate.engines.count, 0.0); + + [delegate removeFlutterEngine:mockEngine]; + XCTAssertEqual(delegate.engines.count, 0.0); +} + +- (void)testUpdateEnginesInSceneRemovesDeallocatedEngine { + FlutterPluginSceneLifeCycleDelegate* delegate = + [[FlutterPluginSceneLifeCycleDelegate alloc] init]; + + @autoreleasepool { + id mockEngine = OCMClassMock([FlutterEngine class]); + [delegate addFlutterEngine:mockEngine]; + XCTAssertEqual(delegate.engines.count, 1.0); + } + + id mockWindowScene = OCMClassMock([UIWindowScene class]); + + [delegate updateEnginesInScene:mockWindowScene]; + XCTAssertEqual(delegate.engines.count, 0.0); +} + +- (void)testUpdateEnginesInSceneRemovesEngineNotInScene { + FlutterPluginSceneLifeCycleDelegate* delegate = + [[FlutterPluginSceneLifeCycleDelegate alloc] init]; + + id mockEngine = OCMClassMock([FlutterEngine class]); + id mockViewController = OCMClassMock([UIViewController class]); + id mockView = OCMClassMock([UIView class]); + id mockWindow = OCMClassMock([UIWindow class]); + id mockWindowScene = OCMClassMock([UIWindowScene class]); + id mockLifecycleProvider = OCMProtocolMock(@protocol(FlutterSceneLifeCycleProvider)); + id mockLifecycleDelegate = OCMClassMock([FlutterPluginSceneLifeCycleDelegate class]); + OCMStub([mockEngine viewController]).andReturn(mockViewController); + OCMStub([mockViewController view]).andReturn(mockView); + OCMStub([mockView window]).andReturn(mockWindow); + OCMStub([mockWindow windowScene]).andReturn(mockWindowScene); + OCMStub([mockWindow windowScene]).andReturn(mockWindowScene); + OCMStub([mockWindowScene delegate]).andReturn(mockLifecycleProvider); + OCMStub([mockLifecycleProvider sceneLifeCycleDelegate]).andReturn(mockLifecycleDelegate); + + [delegate addFlutterEngine:mockEngine]; + XCTAssertEqual(delegate.engines.count, 1.0); + + id mockWindowScene2 = OCMClassMock([UIWindowScene class]); + + [delegate updateEnginesInScene:mockWindowScene2]; + OCMVerify(times(1), [mockLifecycleDelegate addFlutterEngine:mockEngine]); + XCTAssertEqual(delegate.engines.count, 0.0); +} + +- (void)testUpdateEnginesInSceneDoesNotRemoveEngineWithNilScene { + FlutterPluginSceneLifeCycleDelegate* delegate = + [[FlutterPluginSceneLifeCycleDelegate alloc] init]; + id mockEngine = OCMClassMock([FlutterEngine class]); + [delegate addFlutterEngine:mockEngine]; + XCTAssertEqual(delegate.engines.count, 1.0); + + id mockWindowScene = OCMClassMock([UIWindowScene class]); + + [delegate updateEnginesInScene:mockWindowScene]; + XCTAssertEqual(delegate.engines.count, 1.0); +} + +@end diff --git a/engine/src/flutter/shell/platform/darwin/ios/framework/Source/FlutterSceneLifecycle_Test.h b/engine/src/flutter/shell/platform/darwin/ios/framework/Source/FlutterSceneLifecycle_Test.h new file mode 100644 index 00000000000..bc6c7737d04 --- /dev/null +++ b/engine/src/flutter/shell/platform/darwin/ios/framework/Source/FlutterSceneLifecycle_Test.h @@ -0,0 +1,15 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef FLUTTER_SHELL_PLATFORM_DARWIN_IOS_FRAMEWORK_SOURCE_FLUTTERSCENELIFECYCLE_TEST_H_ +#define FLUTTER_SHELL_PLATFORM_DARWIN_IOS_FRAMEWORK_SOURCE_FLUTTERSCENELIFECYCLE_TEST_H_ + +// Category to add test-only visibility. +@interface FlutterPluginSceneLifeCycleDelegate (Test) +@property(nonatomic, strong) NSPointerArray* engines; + +- (void)updateEnginesInScene:(UIScene*)scene; +@end + +#endif // FLUTTER_SHELL_PLATFORM_DARWIN_IOS_FRAMEWORK_SOURCE_FLUTTERSCENELIFECYCLE_TEST_H_ diff --git a/engine/src/flutter/shell/platform/darwin/ios/framework/Source/FlutterView.mm b/engine/src/flutter/shell/platform/darwin/ios/framework/Source/FlutterView.mm index 61d950b30e7..4e5df7a26f1 100644 --- a/engine/src/flutter/shell/platform/darwin/ios/framework/Source/FlutterView.mm +++ b/engine/src/flutter/shell/platform/darwin/ios/framework/Source/FlutterView.mm @@ -5,6 +5,7 @@ #import "flutter/shell/platform/darwin/ios/framework/Source/FlutterView.h" #include "flutter/fml/platform/darwin/cf_utils.h" +#import "flutter/shell/platform/darwin/ios/framework/Source/FlutterSceneLifecycle.h" #import "flutter/shell/platform/darwin/ios/framework/Source/FlutterSharedApplication.h" #import "flutter/shell/platform/darwin/ios/framework/Source/SemanticsObject.h" @@ -241,4 +242,30 @@ static void PrintWideGamutWarningOnce() { return nil; } +- (void)willMoveToWindow:(UIWindow*)newWindow { + // When a FlutterView moves windows, it may also be moving scenes. Add/remove the FlutterEngine + // from the FlutterSceneLifeCycleProvider.sceneLifeCycleDelegate if it changes scenes. + UIWindowScene* newScene = newWindow.windowScene; + UIWindowScene* previousScene = self.window.windowScene; + if (newScene == previousScene) { + return; + } + if ([newScene.delegate conformsToProtocol:@protocol(FlutterSceneLifeCycleProvider)]) { + id lifeCycleProvider = + (id)newScene.delegate; + [lifeCycleProvider.sceneLifeCycleDelegate addFlutterEngine:(FlutterEngine*)self.delegate]; + } + + if ([previousScene.delegate conformsToProtocol:@protocol(FlutterSceneLifeCycleProvider)]) { + // The window, and therefore windowScene, property may be nil if the receiver does not currently + // reside in any window. This occurs when the receiver has just been removed from its superview + // or when the receiver has just been added to a superview that is not attached to a window. + // Remove the engine from the previous scene if set since it is no longer in that window and + // scene. + id lifeCycleProvider = + (id)previousScene.delegate; + [lifeCycleProvider.sceneLifeCycleDelegate removeFlutterEngine:(FlutterEngine*)self.delegate]; + } +} + @end diff --git a/engine/src/flutter/shell/platform/darwin/ios/framework/Source/FlutterViewTest.mm b/engine/src/flutter/shell/platform/darwin/ios/framework/Source/FlutterViewTest.mm index 3b686dea826..c04da93b167 100644 --- a/engine/src/flutter/shell/platform/darwin/ios/framework/Source/FlutterViewTest.mm +++ b/engine/src/flutter/shell/platform/darwin/ios/framework/Source/FlutterViewTest.mm @@ -2,8 +2,13 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +#import #import +#import "flutter/shell/platform/darwin/ios/framework/Headers/FlutterEngine.h" +#import "flutter/shell/platform/darwin/ios/framework/Headers/FlutterSceneDelegate.h" +#import "flutter/shell/platform/darwin/ios/framework/Source/FlutterSceneLifecycle.h" +#import "flutter/shell/platform/darwin/ios/framework/Source/FlutterSceneLifecycle_Test.h" #import "flutter/shell/platform/darwin/ios/framework/Source/FlutterView.h" FLUTTER_ASSERT_ARC @@ -64,4 +69,155 @@ FLUTTER_ASSERT_ARC XCTAssertEqual(view.layer.rasterizationScale, screen.scale); } +- (void)testViewWillMoveToWindow { + NSDictionary* mocks = [self createWindowMocks]; + FlutterView* view = (FlutterView*)mocks[@"view"]; + id mockLifecycleDelegate = mocks[@"mockLifecycleDelegate"]; + FlutterPluginSceneLifeCycleDelegate* lifecycleDelegate = + (FlutterPluginSceneLifeCycleDelegate*)mocks[@"lifecycleDelegate"]; + id mockEngine = mocks[@"mockEngine"]; + id mockWindow = mocks[@"mockWindow"]; + + [view willMoveToWindow:mockWindow]; + OCMVerify(times(1), [mockLifecycleDelegate addFlutterEngine:mockEngine]); + XCTAssertEqual(lifecycleDelegate.engines.count, 1.0); +} + +- (void)testViewWillMoveToSameWindow { + NSDictionary* mocks = [self createWindowMocks]; + FlutterView* view = (FlutterView*)mocks[@"view"]; + id mockLifecycleDelegate = mocks[@"mockLifecycleDelegate"]; + FlutterPluginSceneLifeCycleDelegate* lifecycleDelegate = + (FlutterPluginSceneLifeCycleDelegate*)mocks[@"lifecycleDelegate"]; + id mockEngine = mocks[@"mockEngine"]; + id mockWindow = mocks[@"mockWindow"]; + + [view willMoveToWindow:mockWindow]; + [view willMoveToWindow:mockWindow]; + + OCMVerify(times(2), [mockLifecycleDelegate addFlutterEngine:mockEngine]); + XCTAssertEqual(lifecycleDelegate.engines.count, 1.0); +} + +- (void)testMultipleViewsWillMoveToSameWindow { + NSDictionary* mocks = [self createWindowMocks]; + FlutterView* view1 = (FlutterView*)mocks[@"view"]; + id mockLifecycleDelegate = mocks[@"mockLifecycleDelegate"]; + FlutterPluginSceneLifeCycleDelegate* lifecycleDelegate = + (FlutterPluginSceneLifeCycleDelegate*)mocks[@"lifecycleDelegate"]; + id mockEngine1 = mocks[@"mockEngine"]; + id mockWindow1 = mocks[@"mockWindow"]; + + id mockEngine2 = OCMClassMock([FlutterEngine class]); + FlutterView* view2 = [[FlutterView alloc] initWithDelegate:mockEngine2 + opaque:NO + enableWideGamut:NO]; + + [view1 willMoveToWindow:mockWindow1]; + [view2 willMoveToWindow:mockWindow1]; + [view1 willMoveToWindow:mockWindow1]; + OCMVerify(times(2), [mockLifecycleDelegate addFlutterEngine:mockEngine1]); + OCMVerify(times(1), [mockLifecycleDelegate addFlutterEngine:mockEngine2]); + XCTAssertEqual(lifecycleDelegate.engines.count, 2.0); +} + +- (void)testMultipleViewsWillMoveToDifferentWindow { + NSDictionary* mocks = [self createWindowMocks]; + FlutterView* view1 = (FlutterView*)mocks[@"view"]; + id mockLifecycleDelegate1 = mocks[@"mockLifecycleDelegate"]; + FlutterPluginSceneLifeCycleDelegate* lifecycleDelegate1 = + (FlutterPluginSceneLifeCycleDelegate*)mocks[@"lifecycleDelegate"]; + id mockEngine1 = mocks[@"mockEngine"]; + id mockWindow1 = mocks[@"mockWindow"]; + + NSDictionary* mocks2 = [self createWindowMocks]; + FlutterView* view2 = (FlutterView*)mocks2[@"view"]; + id mockLifecycleDelegate2 = mocks2[@"mockLifecycleDelegate"]; + FlutterPluginSceneLifeCycleDelegate* lifecycleDelegate2 = + (FlutterPluginSceneLifeCycleDelegate*)mocks2[@"lifecycleDelegate"]; + id mockEngine2 = mocks2[@"mockEngine"]; + id mockWindow2 = mocks2[@"mockWindow"]; + + [view1 willMoveToWindow:mockWindow1]; + [view2 willMoveToWindow:mockWindow2]; + [view1 willMoveToWindow:mockWindow1]; + OCMVerify(times(2), [mockLifecycleDelegate1 addFlutterEngine:mockEngine1]); + OCMVerify(times(1), [mockLifecycleDelegate2 addFlutterEngine:mockEngine2]); + XCTAssertEqual(lifecycleDelegate1.engines.count, 1.0); + XCTAssertEqual(lifecycleDelegate2.engines.count, 1.0); +} + +- (void)testNilWindowForViewWhenNoPrevious { + id mockEngine = OCMClassMock([FlutterEngine class]); + FlutterView* view = [[FlutterView alloc] initWithDelegate:mockEngine + opaque:NO + enableWideGamut:NO]; + [view willMoveToWindow:nil]; +} + +- (void)testNilWindowForViewWhenPrevious { + NSDictionary* mocks = [self createWindowMocks]; + FlutterView* view = (FlutterView*)mocks[@"view"]; + id mockLifecycleDelegate = mocks[@"mockLifecycleDelegate"]; + FlutterPluginSceneLifeCycleDelegate* lifecycleDelegate = + (FlutterPluginSceneLifeCycleDelegate*)mocks[@"lifecycleDelegate"]; + id mockEngine = mocks[@"mockEngine"]; + id mockWindow = mocks[@"mockWindow"]; + + id mockView = OCMPartialMock(view); + OCMStub([mockView window]).andReturn(mockWindow); + + [mockView willMoveToWindow:nil]; + + OCMVerify(times(1), [mockLifecycleDelegate removeFlutterEngine:mockEngine]); + XCTAssertEqual(lifecycleDelegate.engines.count, 0.0); +} + +- (void)testViewWillMoveToWindowWhenPreviousEqualsNew { + NSDictionary* mocks = [self createWindowMocks]; + FlutterView* view = (FlutterView*)mocks[@"view"]; + id mockLifecycleDelegate = mocks[@"mockLifecycleDelegate"]; + FlutterPluginSceneLifeCycleDelegate* lifecycleDelegate = + (FlutterPluginSceneLifeCycleDelegate*)mocks[@"lifecycleDelegate"]; + id mockEngine = mocks[@"mockEngine"]; + id mockWindow = mocks[@"mockWindow"]; + + id mockView = OCMPartialMock(view); + OCMStub([mockView window]).andReturn(mockWindow); + + [mockView willMoveToWindow:mockWindow]; + + OCMVerify(times(0), [mockLifecycleDelegate addFlutterEngine:mockEngine]); + OCMVerify(times(0), [mockLifecycleDelegate removeFlutterEngine:[OCMArg any]]); + XCTAssertEqual(lifecycleDelegate.engines.count, 0.0); +} + +- (NSDictionary*)createWindowMocks { + id mockEngine = OCMClassMock([FlutterEngine class]); + FlutterView* view = [[FlutterView alloc] initWithDelegate:mockEngine + opaque:NO + enableWideGamut:NO]; + id mockWindow = OCMClassMock([UIWindow class]); + id mockWindowScene = OCMClassMock([UIWindowScene class]); + + FlutterSceneDelegate* sceneDelegate = [[FlutterSceneDelegate alloc] init]; + id mockSceneDelegate = OCMPartialMock(sceneDelegate); + + FlutterPluginSceneLifeCycleDelegate* lifecycleDelegate = + [[FlutterPluginSceneLifeCycleDelegate alloc] init]; + id mockLifecycleDelegate = OCMPartialMock(lifecycleDelegate); + + OCMStub([mockWindow windowScene]).andReturn(mockWindowScene); + OCMStub([mockWindowScene delegate]).andReturn(mockSceneDelegate); + OCMStub([mockSceneDelegate sceneLifeCycleDelegate]).andReturn(mockLifecycleDelegate); + + return @{ + @"view" : view, + @"mockLifecycleDelegate" : mockLifecycleDelegate, + @"lifecycleDelegate" : lifecycleDelegate, + @"mockEngine" : mockEngine, + @"mockWindow" : mockWindow, + }; +} + @end diff --git a/engine/src/flutter/testing/ios/IosUnitTests/IosUnitTests.xcodeproj/project.pbxproj b/engine/src/flutter/testing/ios/IosUnitTests/IosUnitTests.xcodeproj/project.pbxproj index 5e7f50c0ea7..f83b36bce7d 100644 --- a/engine/src/flutter/testing/ios/IosUnitTests/IosUnitTests.xcodeproj/project.pbxproj +++ b/engine/src/flutter/testing/ios/IosUnitTests/IosUnitTests.xcodeproj/project.pbxproj @@ -75,6 +75,8 @@ 689EC1E2281B30D3008FEB58 /* FlutterSpellCheckPluginTest.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = FlutterSpellCheckPluginTest.mm; sourceTree = ""; }; 68B6091227F62F990036AC78 /* VsyncWaiterIosTest.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = VsyncWaiterIosTest.mm; sourceTree = ""; }; 7802C4F12E67A0CC002C7D6D /* FlutterSceneDelegateTest.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FlutterSceneDelegateTest.m; sourceTree = ""; }; + 78136AF72E68D14D00900DCE /* FlutterSceneLifecycleTest.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = FlutterSceneLifecycleTest.mm; sourceTree = ""; }; + 78A9D2512E6F56DA00BEE2FC /* FlutterViewTest.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = FlutterViewTest.mm; sourceTree = ""; }; 78E4ED342D88A77C00FD954E /* FlutterSharedApplicationTest.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = FlutterSharedApplicationTest.mm; sourceTree = ""; }; D2D361A52B234EAC0018964E /* FlutterMetalLayerTest.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = FlutterMetalLayerTest.mm; sourceTree = ""; }; F7521D7226BB671E005F15C5 /* libios_test_flutter.dylib */ = {isa = PBXFileReference; lastKnownFileType = "compiled.mach-o.dylib"; name = libios_test_flutter.dylib; path = "../../../../out/$(FLUTTER_ENGINE)/libios_test_flutter.dylib"; sourceTree = ""; }; @@ -105,6 +107,7 @@ 0AC232E924BA71D300A85907 /* Source */ = { isa = PBXGroup; children = ( + 78136AF72E68D14D00900DCE /* FlutterSceneLifecycleTest.mm */, 7802C4F12E67A0CC002C7D6D /* FlutterSceneDelegateTest.m */, 78E4ED342D88A77C00FD954E /* FlutterSharedApplicationTest.mm */, F76A3A892BE48F2F00A654F1 /* FlutterPlatformViewsTest.mm */, @@ -114,6 +117,7 @@ F7A3FDE026B9E0A300EADD61 /* FlutterAppDelegateTest.mm */, 0AC232F424BA71D300A85907 /* SemanticsObjectTest.mm */, 0AC232F724BA71D300A85907 /* FlutterEngineTest.mm */, + 78A9D2512E6F56DA00BEE2FC /* FlutterViewTest.mm */, 0AC2330324BA71D300A85907 /* accessibility_bridge_test.mm */, 0AC2330B24BA71D300A85907 /* FlutterTextInputPluginTest.mm */, 0AC2331024BA71D300A85907 /* connection_collection_test.mm */,