Connect the FlutterEngine to the FlutterSceneDelegate (#174910)

Once the `FlutterViewController` is able to get its `UIWindowScene`,
pass itself to the `FlutterSceneDelegate` through the
`FlutterSceneLifeCycleProvider` protocol. The scene will store the
`FlutterEngine` in a weak pointer array.

This is an incremental change towards
https://github.com/flutter/flutter/issues/174398.

Fixes https://github.com/flutter/flutter/issues/174395.

## Pre-launch Checklist

- [x] I read the [Contributor Guide] and followed the process outlined
there for submitting PRs.
- [x] I read the [Tree Hygiene] wiki page, which explains my
responsibilities.
- [x] I read and followed the [Flutter Style Guide], including [Features
we expect every widget to implement].
- [x] I signed the [CLA].
- [x] I listed at least one issue that this PR fixes in the description
above.
- [x] I updated/added relevant documentation (doc comments with `///`).
- [x] I added new tests to check the change I am making, or this PR is
[test-exempt].
- [x] I followed the [breaking change policy] and added [Data Driven
Fixes] where supported.
- [x] All existing and new tests are passing.

If you need help, consider asking for advice on the #hackers-new channel
on [Discord].

**Note**: The Flutter team is currently trialing the use of [Gemini Code
Assist for
GitHub](https://developers.google.com/gemini-code-assist/docs/review-github-code).
Comments from the `gemini-code-assist` bot should not be taken as
authoritative feedback from the Flutter team. If you find its comments
useful you can update your code accordingly, but if you are unsure or
disagree with the feedback, please feel free to wait for a Flutter team
member's review for guidance on which automated comments should be
addressed.

<!-- Links -->
[Contributor Guide]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#overview
[Tree Hygiene]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md
[test-exempt]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#tests
[Flutter Style Guide]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md
[Features we expect every widget to implement]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md#features-we-expect-every-widget-to-implement
[CLA]: https://cla.developers.google.com/
[flutter/tests]: https://github.com/flutter/tests
[breaking change policy]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#handling-breaking-changes
[Discord]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Chat.md
[Data Driven Fixes]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Data-driven-Fixes.md
This commit is contained in:
Victoria Ashworth 2025-09-18 09:56:05 -05:00 committed by GitHub
parent ef6c1d1851
commit ba2bcd62dc
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 473 additions and 0 deletions

View File

@ -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",

View File

@ -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 () <FlutterSceneLifeCycleProvider>
@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 {

View File

@ -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_

View File

@ -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<FlutterSceneLifeCycleProvider> lifeCycleProvider =
(id<FlutterSceneLifeCycleProvider>)engineScene.delegate;
[lifeCycleProvider.sceneLifeCycleDelegate addFlutterEngine:engine];
}
continue;
}
}
}
@end

View File

@ -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 <Foundation/Foundation.h>
#import <OCMock/OCMock.h>
#import <XCTest/XCTest.h>
#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

View File

@ -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_

View File

@ -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<FlutterSceneLifeCycleProvider> lifeCycleProvider =
(id<FlutterSceneLifeCycleProvider>)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<FlutterSceneLifeCycleProvider> lifeCycleProvider =
(id<FlutterSceneLifeCycleProvider>)previousScene.delegate;
[lifeCycleProvider.sceneLifeCycleDelegate removeFlutterEngine:(FlutterEngine*)self.delegate];
}
}
@end

View File

@ -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 <OCMock/OCMock.h>
#import <XCTest/XCTest.h>
#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

View File

@ -75,6 +75,8 @@
689EC1E2281B30D3008FEB58 /* FlutterSpellCheckPluginTest.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = FlutterSpellCheckPluginTest.mm; sourceTree = "<group>"; };
68B6091227F62F990036AC78 /* VsyncWaiterIosTest.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = VsyncWaiterIosTest.mm; sourceTree = "<group>"; };
7802C4F12E67A0CC002C7D6D /* FlutterSceneDelegateTest.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FlutterSceneDelegateTest.m; sourceTree = "<group>"; };
78136AF72E68D14D00900DCE /* FlutterSceneLifecycleTest.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = FlutterSceneLifecycleTest.mm; sourceTree = "<group>"; };
78A9D2512E6F56DA00BEE2FC /* FlutterViewTest.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = FlutterViewTest.mm; sourceTree = "<group>"; };
78E4ED342D88A77C00FD954E /* FlutterSharedApplicationTest.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = FlutterSharedApplicationTest.mm; sourceTree = "<group>"; };
D2D361A52B234EAC0018964E /* FlutterMetalLayerTest.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = FlutterMetalLayerTest.mm; sourceTree = "<group>"; };
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 = "<group>"; };
@ -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 */,