diff --git a/shell/platform/darwin/macos/framework/Headers/FlutterEngine.h b/shell/platform/darwin/macos/framework/Headers/FlutterEngine.h index 957cae047a46f..7c7bea715893b 100644 --- a/shell/platform/darwin/macos/framework/Headers/FlutterEngine.h +++ b/shell/platform/darwin/macos/framework/Headers/FlutterEngine.h @@ -32,6 +32,9 @@ extern const uint64_t kFlutterDefaultViewId; /** * Coordinates a single instance of execution of a Flutter engine. + * + * A FlutterEngine can only be attached with one controller from the native + * code. */ FLUTTER_DARWIN_EXPORT @interface FlutterEngine : NSObject @@ -76,10 +79,9 @@ FLUTTER_DARWIN_EXPORT - (BOOL)runWithEntrypoint:(nullable NSString*)entrypoint; /** - * The default `FlutterViewController` associated with this engine, if any. + * The `FlutterViewController` of this engine, if any. * - * The default view always has ID kFlutterDefaultViewId, and is the view - * operated by the APIs that do not have a view ID specified. + * This view is used by legacy APIs that assume a single view. * * Setting this field from nil to a non-nil view controller also updates * the view controller's engine and ID. diff --git a/shell/platform/darwin/macos/framework/Headers/FlutterPluginRegistrarMacOS.h b/shell/platform/darwin/macos/framework/Headers/FlutterPluginRegistrarMacOS.h index c341f2354dcd9..39fbba1b43bd3 100644 --- a/shell/platform/darwin/macos/framework/Headers/FlutterPluginRegistrarMacOS.h +++ b/shell/platform/darwin/macos/framework/Headers/FlutterPluginRegistrarMacOS.h @@ -36,13 +36,19 @@ FLUTTER_DARWIN_EXPORT @property(nonnull, readonly) id textures; /** - * The view displaying Flutter content. May return |nil|, for instance in a headless environment. + * The default view displaying Flutter content. * - * WARNING: If/when multiple Flutter views within the same application are supported (#30701), this - * API will change. + * This method may return |nil|, for instance in a headless environment. + * + * The default view is a special view operated by single-view APIs. */ @property(nullable, readonly) NSView* view; +/** + * The `NSView` associated with the given view ID, if any. + */ +- (nullable NSView*)viewForId:(uint64_t)viewId; + /** * Registers |delegate| to receive handleMethodCall:result: callbacks for the given |channel|. */ diff --git a/shell/platform/darwin/macos/framework/Headers/FlutterViewController.h b/shell/platform/darwin/macos/framework/Headers/FlutterViewController.h index e4d8df7d4c48d..5979131f7451b 100644 --- a/shell/platform/darwin/macos/framework/Headers/FlutterViewController.h +++ b/shell/platform/darwin/macos/framework/Headers/FlutterViewController.h @@ -89,7 +89,9 @@ FLUTTER_DARWIN_EXPORT NS_DESIGNATED_INITIALIZER; - (nonnull instancetype)initWithCoder:(nonnull NSCoder*)nibNameOrNil NS_DESIGNATED_INITIALIZER; /** - * Initializes this FlutterViewController with the specified `FlutterEngine`. + * Initializes this FlutterViewController with an existing `FlutterEngine`. + * + * The initialized view controller will add itself to the engine as part of this process. * * This initializer is suitable for both the first Flutter view controller and * the following ones of the app. diff --git a/shell/platform/darwin/macos/framework/Source/FlutterCompositor.mm b/shell/platform/darwin/macos/framework/Source/FlutterCompositor.mm index 74173158d8c59..51b874f47dfbd 100644 --- a/shell/platform/darwin/macos/framework/Source/FlutterCompositor.mm +++ b/shell/platform/darwin/macos/framework/Source/FlutterCompositor.mm @@ -19,7 +19,7 @@ // TODO(dkwingsmt): This class only supports single-view for now. As more // classes are gradually converted to multi-view, it should get the view ID // from somewhere. - FlutterView* view = [view_provider_ getView:kFlutterDefaultViewId]; + FlutterView* view = [view_provider_ viewForId:kFlutterDefaultViewId]; if (!view) { return false; } @@ -37,7 +37,7 @@ bool FlutterCompositor::Present(uint64_t view_id, const FlutterLayer** layers, size_t layers_count) { - FlutterView* view = [view_provider_ getView:view_id]; + FlutterView* view = [view_provider_ viewForId:view_id]; if (!view) { return false; } diff --git a/shell/platform/darwin/macos/framework/Source/FlutterCompositorTest.mm b/shell/platform/darwin/macos/framework/Source/FlutterCompositorTest.mm index bd6bf4d57b58b..4005b176aaf24 100644 --- a/shell/platform/darwin/macos/framework/Source/FlutterCompositorTest.mm +++ b/shell/platform/darwin/macos/framework/Source/FlutterCompositorTest.mm @@ -11,6 +11,8 @@ #import "flutter/shell/platform/darwin/macos/framework/Source/FlutterViewProvider.h" #import "flutter/testing/testing.h" +extern const uint64_t kFlutterDefaultViewId; + @interface FlutterViewMockProvider : NSObject { FlutterView* _defaultView; } @@ -30,7 +32,7 @@ - (nonnull instancetype)initWithDefaultView:(nonnull FlutterView*)view { return self; } -- (nullable FlutterView*)getView:(uint64_t)viewId { +- (nullable FlutterView*)viewForId:(uint64_t)viewId { if (viewId == kFlutterDefaultViewId) { return _defaultView; } diff --git a/shell/platform/darwin/macos/framework/Source/FlutterEngine.mm b/shell/platform/darwin/macos/framework/Source/FlutterEngine.mm index 9fc17bd2e0af5..2c5aef3a262fb 100644 --- a/shell/platform/darwin/macos/framework/Source/FlutterEngine.mm +++ b/shell/platform/darwin/macos/framework/Source/FlutterEngine.mm @@ -84,6 +84,29 @@ @interface FlutterEngine () - (nullable FlutterViewController*)viewControllerForId:(uint64_t)viewId; +/** + * An internal method that adds the view controller with the given ID. + * + * This method assigns the controller with the ID, puts the controller into the + * map, and does assertions related to the default view ID. + */ +- (void)registerViewController:(FlutterViewController*)controller forId:(uint64_t)viewId; + +/** + * An internal method that removes the view controller with the given ID. + * + * This method clears the ID of the controller, removes the controller from the + * map. This is an no-op if the view ID is not associated with any view + * controllers. + */ +- (void)deregisterViewControllerForId:(uint64_t)viewId; + +/** + * Shuts down the engine if view requirement is not met, and headless execution + * is not allowed. + */ +- (void)shutDownIfNeeded; + /** * Sends the list of user-preferred locales to the Flutter engine. */ @@ -141,6 +164,8 @@ @implementation FlutterEngineRegistrar { FlutterEngine* _flutterEngine; } +@dynamic view; + - (instancetype)initWithPlugin:(NSString*)pluginKey flutterEngine:(FlutterEngine*)flutterEngine { self = [super init]; if (self) { @@ -161,10 +186,18 @@ - (instancetype)initWithPlugin:(NSString*)pluginKey flutterEngine:(FlutterEngine } - (NSView*)view { - if (!_flutterEngine.viewController.viewLoaded) { - [_flutterEngine.viewController loadView]; + return [self viewForId:kFlutterDefaultViewId]; +} + +- (NSView*)viewForId:(uint64_t)viewId { + FlutterViewController* controller = [_flutterEngine viewControllerForId:viewId]; + if (controller == nil) { + return nil; + } + if (!controller.viewLoaded) { + [controller loadView]; } - return _flutterEngine.viewController.flutterView; + return controller.flutterView; } - (void)addMethodCallDelegate:(nonnull id)delegate @@ -214,6 +247,11 @@ @implementation FlutterEngine { // when the engine is destroyed. std::unique_ptr _macOSCompositor; + // The information of all views attached to this engine mapped from IDs. + // + // It can't use NSDictionary, because the values need to be weak references. + NSMapTable* _viewControllers; + // FlutterCompositor is copied and used in embedder.cc. FlutterCompositor _compositor; @@ -230,6 +268,8 @@ @implementation FlutterEngine { // A method channel for miscellaneous platform functionality. FlutterMethodChannel* _platformChannel; + + int _nextViewId; } - (instancetype)initWithName:(NSString*)labelPrefix project:(FlutterDartProject*)project { @@ -249,10 +289,14 @@ - (instancetype)initWithName:(NSString*)labelPrefix _semanticsEnabled = NO; _isResponseValid = [[NSMutableArray alloc] initWithCapacity:1]; [_isResponseValid addObject:@YES]; + // kFlutterDefaultViewId is reserved for the default view. + // All IDs above it are for regular views. + _nextViewId = kFlutterDefaultViewId + 1; _embedderAPI.struct_size = sizeof(FlutterEngineProcTable); FlutterEngineGetProcAddresses(&_embedderAPI); + _viewControllers = [NSMapTable weakToWeakObjectsMapTable]; _renderer = [[FlutterRenderer alloc] initWithFlutterEngine:self]; NSNotificationCenter* notificationCenter = [NSNotificationCenter defaultCenter]; @@ -284,7 +328,7 @@ - (BOOL)runWithEntrypoint:(NSString*)entrypoint { return NO; } - if (!_allowHeadlessExecution && !_viewController) { + if (!_allowHeadlessExecution && [_viewControllers count] == 0) { NSLog(@"Attempted to run an engine with no view controller without headless mode enabled."); return NO; } @@ -311,8 +355,11 @@ - (BOOL)runWithEntrypoint:(NSString*)entrypoint { flutterArguments.platform_message_callback = (FlutterPlatformMessageCallback)OnPlatformMessage; flutterArguments.update_semantics_callback = [](const FlutterSemanticsUpdate* update, void* user_data) { + // TODO(dkwingsmt): This callback only supports single-view, therefore it + // only operates on the default view. To support multi-view, we need a + // way to pass in the ID (probably through FlutterSemanticsUpdate). FlutterEngine* engine = (__bridge FlutterEngine*)user_data; - [engine.viewController updateSemantics:update]; + [[engine viewControllerForId:kFlutterDefaultViewId] updateSemantics:update]; }; flutterArguments.custom_dart_entrypoint = entrypoint.UTF8String; flutterArguments.shutdown_dart_vm_when_done = true; @@ -374,7 +421,14 @@ - (BOOL)runWithEntrypoint:(NSString*)entrypoint { } [self sendUserLocales]; - [self updateWindowMetrics]; + + // Update window metric for all view controllers. + NSEnumerator* viewControllerEnumerator = [_viewControllers objectEnumerator]; + FlutterViewController* nextViewController; + while ((nextViewController = [viewControllerEnumerator nextObject])) { + [self updateWindowMetricsForViewController:nextViewController]; + } + [self updateDisplayConfig]; // Send the initial user settings such as brightness and text scale factor // to the engine. @@ -408,28 +462,59 @@ - (void)loadAOTData:(NSString*)assetsDir { } } +- (void)registerViewController:(FlutterViewController*)controller forId:(uint64_t)viewId { + NSAssert(controller != nil, @"The controller must not be nil."); + NSAssert(![controller attached], + @"The incoming view controller is already attached to an engine."); + NSAssert([_viewControllers objectForKey:@(viewId)] == nil, @"The requested view ID is occupied."); + [controller attachToEngine:self withId:viewId]; + NSAssert(controller.id == viewId, @"Failed to assign view ID."); + [_viewControllers setObject:controller forKey:@(viewId)]; +} + +- (void)deregisterViewControllerForId:(uint64_t)viewId { + FlutterViewController* oldController = [self viewControllerForId:viewId]; + if (oldController != nil) { + [oldController detachFromEngine]; + [_viewControllers removeObjectForKey:@(viewId)]; + } +} + +- (void)shutDownIfNeeded { + if ([_viewControllers count] == 0 && !_allowHeadlessExecution) { + [self shutDownEngine]; + } +} + +- (FlutterViewController*)viewControllerForId:(uint64_t)viewId { + FlutterViewController* controller = [_viewControllers objectForKey:@(viewId)]; + NSAssert(controller == nil || controller.id == viewId, + @"The stored controller has unexpected view ID."); + return controller; +} + - (void)setViewController:(FlutterViewController*)controller { - if (_viewController == controller) { + FlutterViewController* currentController = + [_viewControllers objectForKey:@(kFlutterDefaultViewId)]; + if (currentController == controller) { // From nil to nil, or from non-nil to the same controller. return; } - if (_viewController == nil && controller != nil) { + if (currentController == nil && controller != nil) { // From nil to non-nil. NSAssert(controller.engine == nil, @"Failed to set view controller to the engine: " @"The given FlutterViewController is already attached to an engine %@. " @"If you wanted to create an FlutterViewController and set it to an existing engine, " - @"you should create it with init(engine:, nibName, bundle:) instead.", + @"you should use FlutterViewController#init(engine:, nibName, bundle:) instead.", controller.engine); - _viewController = controller; - [_viewController attachToEngine:self withId:kFlutterDefaultViewId]; - } else if (_viewController != nil && controller == nil) { + [self registerViewController:controller forId:kFlutterDefaultViewId]; + } else if (currentController != nil && controller == nil) { + NSAssert(currentController.id == kFlutterDefaultViewId, + @"The default controller has an unexpected ID %llu", currentController.id); // From non-nil to nil. - [_viewController detachFromEngine]; - _viewController = nil; - if (!_allowHeadlessExecution) { - [self shutDownEngine]; - } + [self deregisterViewControllerForId:kFlutterDefaultViewId]; + [self shutDownIfNeeded]; } else { // From non-nil to a different non-nil view controller. NSAssert(NO, @@ -437,10 +522,14 @@ - (void)setViewController:(FlutterViewController*)controller { @"The engine already has a default view controller %@. " @"If you wanted to make the default view render in a different window, " @"you should attach the current view controller to the window instead.", - _viewController); + [_viewControllers objectForKey:@(kFlutterDefaultViewId)]); } } +- (FlutterViewController*)viewController { + return [self viewControllerForId:kFlutterDefaultViewId]; +} + - (FlutterCompositor*)createFlutterCompositor { _macOSCompositor = std::make_unique( [[FlutterViewEngineProvider alloc] initWithEngine:self], _platformViewController); @@ -485,6 +574,17 @@ - (FlutterCompositor*)createFlutterCompositor { #pragma mark - Framework-internal methods +- (void)addViewController:(FlutterViewController*)controller { + [self registerViewController:controller forId:kFlutterDefaultViewId]; +} + +- (void)removeViewController:(nonnull FlutterViewController*)viewController { + NSAssert([viewController attached] && viewController.engine == self, + @"The given view controller is not associated with this engine."); + [self deregisterViewControllerForId:viewController.id]; + [self shutDownIfNeeded]; +} + - (BOOL)running { return _engine != nullptr; } @@ -544,11 +644,19 @@ - (nonnull NSString*)executableName { return [[[NSProcessInfo processInfo] arguments] firstObject] ?: @"Flutter"; } -- (void)updateWindowMetrics { - if (!_engine || !self.viewController.viewLoaded) { +- (void)updateWindowMetricsForViewController:(FlutterViewController*)viewController { + if (viewController.id != kFlutterDefaultViewId) { + // TODO(dkwingsmt): The embedder API only supports single-view for now. As + // embedder APIs are converted to multi-view, this method should support any + // views. return; } - NSView* view = self.viewController.flutterView; + if (!_engine || !viewController || !viewController.viewLoaded) { + return; + } + NSAssert([self viewControllerForId:viewController.id] == viewController, + @"The provided view controller is not attached to this engine."); + NSView* view = viewController.flutterView; CGRect scaledBounds = [view convertRectToBacking:view.bounds]; CGSize scaledSize = scaledBounds.size; double pixelRatio = view.bounds.size.width == 0 ? 1 : scaledSize.width / view.bounds.size.width; @@ -579,7 +687,14 @@ - (void)setSemanticsEnabled:(BOOL)enabled { return; } _semanticsEnabled = enabled; - [_viewController notifySemanticsEnabledChanged]; + + // Update all view controllers' bridges. + NSEnumerator* viewControllerEnumerator = [_viewControllers objectEnumerator]; + FlutterViewController* nextViewController; + while ((nextViewController = [viewControllerEnumerator nextObject])) { + [nextViewController notifySemanticsEnabledChanged]; + } + _embedderAPI.UpdateSemanticsEnabled(_engine, _semanticsEnabled); } @@ -595,17 +710,6 @@ - (FlutterPlatformViewController*)platformViewController { #pragma mark - Private methods -- (FlutterViewController*)viewControllerForId:(uint64_t)viewId { - // TODO(dkwingsmt): The engine only supports single-view, therefore it - // only processes the default ID. After the engine supports multiple views, - // this method should be able to return the view for any IDs. - NSAssert(viewId == kFlutterDefaultViewId, @"Unexpected view ID %llu", viewId); - if (viewId == kFlutterDefaultViewId) { - return _viewController; - } - return nil; -} - - (void)sendUserLocales { if (!self.running) { return; @@ -669,8 +773,10 @@ - (void)engineCallbackOnPlatformMessage:(const FlutterPlatformMessage*)message { } - (void)engineCallbackOnPreEngineRestart { - if (_viewController) { - [_viewController onPreEngineRestart]; + NSEnumerator* viewControllerEnumerator = [_viewControllers objectEnumerator]; + FlutterViewController* nextViewController; + while ((nextViewController = [viewControllerEnumerator nextObject])) { + [nextViewController onPreEngineRestart]; } } @@ -682,7 +788,11 @@ - (void)shutDownEngine { return; } - [self.viewController.flutterView shutdown]; + NSEnumerator* viewControllerEnumerator = [_viewControllers objectEnumerator]; + FlutterViewController* nextViewController; + while ((nextViewController = [viewControllerEnumerator nextObject])) { + [nextViewController.flutterView shutdown]; + } FlutterEngineResult result = _embedderAPI.Deinitialize(_engine); if (result != kSuccess) { @@ -747,7 +857,12 @@ - (void)applicationWillTerminate:(NSNotification*)notification { - (void)onAccessibilityStatusChanged:(NSNotification*)notification { BOOL enabled = [notification.userInfo[kEnhancedUserInterfaceKey] boolValue]; - [self.viewController onAccessibilityStatusChanged:enabled]; + NSEnumerator* viewControllerEnumerator = [_viewControllers objectEnumerator]; + FlutterViewController* nextViewController; + while ((nextViewController = [viewControllerEnumerator nextObject])) { + [nextViewController onAccessibilityStatusChanged:enabled]; + } + self.semanticsEnabled = enabled; } diff --git a/shell/platform/darwin/macos/framework/Source/FlutterEngineTest.mm b/shell/platform/darwin/macos/framework/Source/FlutterEngineTest.mm index 911ff76a07eb4..d3b2be0406001 100644 --- a/shell/platform/darwin/macos/framework/Source/FlutterEngineTest.mm +++ b/shell/platform/darwin/macos/framework/Source/FlutterEngineTest.mm @@ -614,6 +614,69 @@ - (nonnull NSView*)createWithViewIdentifier:(int64_t)viewId arguments:(nullable rasterThread.join(); } +TEST_F(FlutterEngineTest, ManageControllersIfInitiatedByController) { + NSString* fixtures = @(flutter::testing::GetFixturesPath()); + FlutterDartProject* project = [[FlutterDartProject alloc] + initWithAssetsPath:fixtures + ICUDataPath:[fixtures stringByAppendingString:@"/icudtl.dat"]]; + + FlutterEngine* engine; + FlutterViewController* viewController1; + + @autoreleasepool { + // Create FVC1. + viewController1 = [[FlutterViewController alloc] initWithProject:project]; + EXPECT_EQ(viewController1.id, 0ull); + + engine = viewController1.engine; + engine.viewController = nil; + + // Create FVC2 based on the same engine. + FlutterViewController* viewController2 = [[FlutterViewController alloc] initWithEngine:engine + nibName:nil + bundle:nil]; + EXPECT_EQ(engine.viewController, viewController2); + } + // FVC2 is deallocated but FVC1 is retained. + + EXPECT_EQ(engine.viewController, nil); + + engine.viewController = viewController1; + EXPECT_EQ(engine.viewController, viewController1); + EXPECT_EQ(viewController1.id, 0ull); +} + +TEST_F(FlutterEngineTest, ManageControllersIfInitiatedByEngine) { + // Don't create the engine with `CreateMockFlutterEngine`, because it adds + // additional references to FlutterViewControllers, which is crucial to this + // test case. + FlutterEngine* engine = [[FlutterEngine alloc] initWithName:@"io.flutter" + project:nil + allowHeadlessExecution:NO]; + FlutterViewController* viewController1; + + @autoreleasepool { + viewController1 = [[FlutterViewController alloc] initWithEngine:engine nibName:nil bundle:nil]; + EXPECT_EQ(viewController1.id, 0ull); + EXPECT_EQ(engine.viewController, viewController1); + + engine.viewController = nil; + + FlutterViewController* viewController2 = [[FlutterViewController alloc] initWithEngine:engine + nibName:nil + bundle:nil]; + EXPECT_EQ(viewController2.id, 0ull); + EXPECT_EQ(engine.viewController, viewController2); + } + // FVC2 is deallocated but FVC1 is retained. + + EXPECT_EQ(engine.viewController, nil); + + engine.viewController = viewController1; + EXPECT_EQ(engine.viewController, viewController1); + EXPECT_EQ(viewController1.id, 0ull); +} + } // namespace flutter::testing // NOLINTEND(clang-analyzer-core.StackAddressEscape) diff --git a/shell/platform/darwin/macos/framework/Source/FlutterEngine_Internal.h b/shell/platform/darwin/macos/framework/Source/FlutterEngine_Internal.h index 6eed864e6a433..1879cb92dbdab 100644 --- a/shell/platform/darwin/macos/framework/Source/FlutterEngine_Internal.h +++ b/shell/platform/darwin/macos/framework/Source/FlutterEngine_Internal.h @@ -48,9 +48,39 @@ @property(nonatomic, readonly, nonnull) NSPasteboard* pasteboard; /** - * Informs the engine that the associated view controller's view size has changed. + * Attach a view controller to the engine as its default controller. + * + * Practically, since FlutterEngine can only be attached with one controller, + * the given controller, if successfully attached, will always have the default + * view ID kFlutterDefaultViewId. + * + * The engine holds a weak reference to the attached view controller. + * + * If the given view controller is already attached to an engine, this call + * throws an assertion. */ -- (void)updateWindowMetrics; +- (void)addViewController:(nonnull FlutterViewController*)viewController; + +/** + * Dissociate the given view controller from this engine. + * + * Practically, since FlutterEngine can only be attached with one controller, + * the given controller must be the default view controller. + * + * If the view controller is not associated with this engine, this call throws an + * assertion. + */ +- (void)removeViewController:(nonnull FlutterViewController*)viewController; + +/** + * The `FlutterViewController` associated with the given view ID, if any. + */ +- (nullable FlutterViewController*)viewControllerForId:(uint64_t)viewId; + +/** + * Informs the engine that the specified view controller's window metrics have changed. + */ +- (void)updateWindowMetricsForViewController:(nonnull FlutterViewController*)viewController; /** * Dispatches the given pointer event data to engine. diff --git a/shell/platform/darwin/macos/framework/Source/FlutterKeyboardManager.mm b/shell/platform/darwin/macos/framework/Source/FlutterKeyboardManager.mm index 78c18dd0b08e0..ad50476a47675 100644 --- a/shell/platform/darwin/macos/framework/Source/FlutterKeyboardManager.mm +++ b/shell/platform/darwin/macos/framework/Source/FlutterKeyboardManager.mm @@ -57,7 +57,7 @@ @interface FlutterKeyboardManager () /** * The text input plugin set by initialization. */ -@property(nonatomic) id viewDelegate; +@property(nonatomic, weak) id viewDelegate; /** * The primary responders added by addPrimaryResponder. diff --git a/shell/platform/darwin/macos/framework/Source/FlutterRenderer.mm b/shell/platform/darwin/macos/framework/Source/FlutterRenderer.mm index 007a3cce157a5..d7d2c2fc7d6ac 100644 --- a/shell/platform/darwin/macos/framework/Source/FlutterRenderer.mm +++ b/shell/platform/darwin/macos/framework/Source/FlutterRenderer.mm @@ -89,7 +89,7 @@ - (FlutterRendererConfig)createRendererConfig { #pragma mark - Embedder callback implementations. - (FlutterMetalTexture)createTextureForView:(uint64_t)viewId size:(CGSize)size { - FlutterView* view = [_viewProvider getView:viewId]; + FlutterView* view = [_viewProvider viewForId:viewId]; NSAssert(view != nil, @"Can't create texture on a non-existent view 0x%llx.", viewId); if (view == nil) { // FlutterMetalTexture has texture `null`, therefore is discarded. @@ -99,7 +99,7 @@ - (FlutterMetalTexture)createTextureForView:(uint64_t)viewId size:(CGSize)size { } - (BOOL)present:(uint64_t)viewId texture:(const FlutterMetalTexture*)texture { - FlutterView* view = [_viewProvider getView:viewId]; + FlutterView* view = [_viewProvider viewForId:viewId]; if (view == nil) { return NO; } diff --git a/shell/platform/darwin/macos/framework/Source/FlutterRendererTest.mm b/shell/platform/darwin/macos/framework/Source/FlutterRendererTest.mm index 01146e1464c02..46da658efd8e6 100644 --- a/shell/platform/darwin/macos/framework/Source/FlutterRendererTest.mm +++ b/shell/platform/darwin/macos/framework/Source/FlutterRendererTest.mm @@ -9,37 +9,54 @@ #import "flutter/shell/platform/darwin/macos/framework/Source/FlutterEngine_Internal.h" #import "flutter/shell/platform/darwin/macos/framework/Source/FlutterRenderer.h" #import "flutter/shell/platform/darwin/macos/framework/Source/FlutterView.h" +#import "flutter/shell/platform/darwin/macos/framework/Source/FlutterViewControllerTestUtils.h" #import "flutter/shell/platform/darwin/macos/framework/Source/FlutterViewController_Internal.h" #include "flutter/shell/platform/embedder/embedder.h" #include "flutter/shell/platform/embedder/test_utils/proc_table_replacement.h" #include "flutter/testing/testing.h" +@interface RendererTestViewController : FlutterViewController +- (void)loadMockFlutterView:(FlutterView*)mockView; +@end + +@implementation RendererTestViewController { + FlutterView* _mockFlutterView; +} + +- (void)loadMockFlutterView:(FlutterView*)mockView { + _mockFlutterView = mockView; + [self loadView]; +} + +- (nonnull FlutterView*)createFlutterViewWithMTLDevice:(id)device + commandQueue:(id)commandQueue { + return _mockFlutterView; +} +@end + namespace flutter::testing { namespace { // Returns an engine configured for the test fixture resource configuration. -FlutterEngine* CreateTestEngine() { +RendererTestViewController* CreateTestViewController() { NSString* fixtures = @(testing::GetFixturesPath()); FlutterDartProject* project = [[FlutterDartProject alloc] initWithAssetsPath:fixtures ICUDataPath:[fixtures stringByAppendingString:@"/icudtl.dat"]]; - return [[FlutterEngine alloc] initWithName:@"test" project:project allowHeadlessExecution:true]; -} - -void SetEngineDefaultView(FlutterEngine* engine, id flutterView) { - id mockFlutterViewController = OCMClassMock([FlutterViewController class]); - OCMStub([mockFlutterViewController flutterView]).andReturn(flutterView); - [engine setViewController:mockFlutterViewController]; + RendererTestViewController* viewController = + [[RendererTestViewController alloc] initWithProject:project]; + return viewController; } } // namespace TEST(FlutterRenderer, PresentDelegatesToFlutterView) { - FlutterEngine* engine = CreateTestEngine(); - FlutterRenderer* renderer = [[FlutterRenderer alloc] initWithFlutterEngine:engine]; + RendererTestViewController* viewController = CreateTestViewController(); + FlutterEngine* engine = viewController.engine; id viewMock = OCMClassMock([FlutterView class]); - SetEngineDefaultView(engine, viewMock); + [viewController loadMockFlutterView:viewMock]; + FlutterRenderer* renderer = [[FlutterRenderer alloc] initWithFlutterEngine:engine]; id surfaceManagerMock = OCMClassMock([FlutterSurfaceManager class]); OCMStub([viewMock surfaceManager]).andReturn(surfaceManagerMock); @@ -61,11 +78,12 @@ void SetEngineDefaultView(FlutterEngine* engine, id flutterView) { } TEST(FlutterRenderer, TextureReturnedByFlutterView) { - FlutterEngine* engine = CreateTestEngine(); - FlutterRenderer* renderer = [[FlutterRenderer alloc] initWithFlutterEngine:engine]; + RendererTestViewController* viewController = CreateTestViewController(); + FlutterEngine* engine = viewController.engine; id viewMock = OCMClassMock([FlutterView class]); - SetEngineDefaultView(engine, viewMock); + [viewController loadMockFlutterView:viewMock]; + FlutterRenderer* renderer = [[FlutterRenderer alloc] initWithFlutterEngine:engine]; id surfaceManagerMock = OCMClassMock([FlutterSurfaceManager class]); OCMStub([viewMock surfaceManager]).andReturn(surfaceManagerMock); diff --git a/shell/platform/darwin/macos/framework/Source/FlutterViewController.mm b/shell/platform/darwin/macos/framework/Source/FlutterViewController.mm index 72d67f105e264..994cc8f468d69 100644 --- a/shell/platform/darwin/macos/framework/Source/FlutterViewController.mm +++ b/shell/platform/darwin/macos/framework/Source/FlutterViewController.mm @@ -309,11 +309,12 @@ static void CommonInit(FlutterViewController* controller, FlutterEngine* engine) @"The FlutterViewController is unexpectedly attached to " @"engine %@ before initialization.", controller.engine); - engine.viewController = controller; + [engine addViewController:controller]; NSCAssert(controller.engine != nil, @"The FlutterViewController unexpectedly stays unattached after initialization. " @"In unit tests, this is likely because either the FlutterViewController or " - @"the FlutterEngine is mocked. Please subclass these classes instead."); + @"the FlutterEngine is mocked. Please subclass these classes instead.", + controller.engine, controller.id); controller->_mouseTrackingMode = FlutterMouseTrackingModeInKeyWindow; controller->_textInputPlugin = [[FlutterTextInputPlugin alloc] initWithViewController:controller]; [controller initializeKeyboard]; @@ -355,11 +356,6 @@ - (instancetype)initWithEngine:(nonnull FlutterEngine*)engine nibName:(nullable NSString*)nibName bundle:(nullable NSBundle*)nibBundle { NSAssert(engine != nil, @"Engine is required"); - NSAssert(engine.viewController == nil, - @"The supplied FlutterEngine is already used with FlutterViewController " - "instance. One instance of the FlutterEngine can only be attached to one " - "FlutterViewController at a time. Set FlutterEngine.viewController " - "to nil before attaching it to another FlutterViewController."); self = [super initWithNibName:nibName bundle:nibBundle]; if (self) { @@ -412,7 +408,9 @@ - (void)viewWillDisappear { } - (void)dealloc { - _engine.viewController = nil; + if ([self attached]) { + [_engine removeViewController:self]; + } CFNotificationCenterRef cfCenter = CFNotificationCenterGetDistributedCenter(); CFNotificationCenterRemoveEveryObserver(cfCenter, (__bridge void*)self); } @@ -583,8 +581,7 @@ - (void)configureTrackingArea { - (void)initializeKeyboard { // TODO(goderbauer): Seperate keyboard/textinput stuff into ViewController specific and Engine // global parts. Move the global parts to FlutterEngine. - __weak FlutterViewController* weakSelf = self; - _keyboardManager = [[FlutterKeyboardManager alloc] initWithViewDelegate:weakSelf]; + _keyboardManager = [[FlutterKeyboardManager alloc] initWithViewDelegate:self]; } - (void)dispatchMouseEvent:(nonnull NSEvent*)event { @@ -795,7 +792,7 @@ - (void)onKeyboardLayoutChanged { * Responds to view reshape by notifying the engine of the change in dimensions. */ - (void)viewDidReshape:(NSView*)view { - [_engine updateWindowMetrics]; + [_engine updateWindowMetricsForViewController:self]; } #pragma mark - FlutterPluginRegistry diff --git a/shell/platform/darwin/macos/framework/Source/FlutterViewEngineProvider.mm b/shell/platform/darwin/macos/framework/Source/FlutterViewEngineProvider.mm index a063784b5e2c9..95c3aa53e0ee5 100644 --- a/shell/platform/darwin/macos/framework/Source/FlutterViewEngineProvider.mm +++ b/shell/platform/darwin/macos/framework/Source/FlutterViewEngineProvider.mm @@ -22,14 +22,8 @@ - (instancetype)initWithEngine:(FlutterEngine*)engine { return self; } -- (nullable FlutterView*)getView:(uint64_t)viewId { - // TODO(dkwingsmt): This class only supports the first view for now. After - // FlutterEngine supports multi-view, it should get the view associated to the - // ID. - if (viewId == kFlutterDefaultViewId) { - return _engine.viewController.flutterView; - } - return nil; +- (nullable FlutterView*)viewForId:(uint64_t)viewId { + return [_engine viewControllerForId:viewId].flutterView; } @end diff --git a/shell/platform/darwin/macos/framework/Source/FlutterViewEngineProviderTest.mm b/shell/platform/darwin/macos/framework/Source/FlutterViewEngineProviderTest.mm index 6a1aa66b69c59..86c94a70caa60 100644 --- a/shell/platform/darwin/macos/framework/Source/FlutterViewEngineProviderTest.mm +++ b/shell/platform/darwin/macos/framework/Source/FlutterViewEngineProviderTest.mm @@ -6,6 +6,7 @@ #import #import +#import "flutter/shell/platform/darwin/macos/framework/Source/FlutterEngineTestUtils.h" #import "flutter/shell/platform/darwin/macos/framework/Source/FlutterEngine_Internal.h" #import "flutter/shell/platform/darwin/macos/framework/Source/FlutterViewController_Internal.h" #import "flutter/shell/platform/darwin/macos/framework/Source/FlutterViewEngineProvider.h" @@ -19,23 +20,29 @@ TEST(FlutterViewEngineProviderUnittests, GetViewReturnsTheCorrectView) { FlutterViewEngineProvider* viewProvider; - id mockEngine = OCMClassMock([FlutterEngine class]); + id mockEngine = CreateMockFlutterEngine(@""); __block id mockFlutterViewController; - OCMStub([mockEngine viewController]).andDo(^(NSInvocation* invocation) { - if (mockFlutterViewController != nil) { - [invocation setReturnValue:&mockFlutterViewController]; - } - }); + OCMStub([mockEngine viewControllerForId:0]) + .ignoringNonObjectArgs() + .andDo(^(NSInvocation* invocation) { + uint64_t viewId; + [invocation getArgument:&viewId atIndex:2]; + if (viewId == 0 /* kFlutterDefaultViewId */) { + if (mockFlutterViewController != nil) { + [invocation setReturnValue:&mockFlutterViewController]; + } + } + }); viewProvider = [[FlutterViewEngineProvider alloc] initWithEngine:mockEngine]; // When the view controller is not set, the returned view is nil. - EXPECT_EQ([viewProvider getView:0], nil); + EXPECT_EQ([viewProvider viewForId:0], nil); // When the view controller is set, the returned view is the controller's view. mockFlutterViewController = OCMStrictClassMock([FlutterViewController class]); id mockView = OCMStrictClassMock([FlutterView class]); OCMStub([mockFlutterViewController flutterView]).andReturn(mockView); - EXPECT_EQ([viewProvider getView:0], mockView); + EXPECT_EQ([viewProvider viewForId:0], mockView); } } // namespace flutter::testing diff --git a/shell/platform/darwin/macos/framework/Source/FlutterViewProvider.h b/shell/platform/darwin/macos/framework/Source/FlutterViewProvider.h index f873c72266a95..9548b7afab1b8 100644 --- a/shell/platform/darwin/macos/framework/Source/FlutterViewProvider.h +++ b/shell/platform/darwin/macos/framework/Source/FlutterViewProvider.h @@ -20,6 +20,6 @@ extern const uint64_t kFlutterDefaultViewId; * * Returns nil if the ID is invalid. */ -- (nullable FlutterView*)getView:(uint64_t)id; +- (nullable FlutterView*)viewForId:(uint64_t)id; @end