[macOS] Make FlutterEngine support multiple views (flutter/engine#37976)

* Rebase all

* Do not remove views on hot restart

* Remove log, fix doc

* Apply suggestions from code review

Co-authored-by: Loïc Sharma <737941+loic-sharma@users.noreply.github.com>

* getDefaultView

* postpone deprecation. Add TODO

* Apply suggestions from code review

Co-authored-by: Loïc Sharma <737941+loic-sharma@users.noreply.github.com>

* Change to single

* Update FlutterEngine.h

* Update FlutterEngine_Internal.h

---------

Co-authored-by: Loïc Sharma <737941+loic-sharma@users.noreply.github.com>
This commit is contained in:
Tong Mu
2023-02-09 14:58:19 -08:00
committed by GitHub
parent f5f5854e7f
commit 9aeba94a67
15 changed files with 329 additions and 95 deletions

View File

@@ -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 <FlutterTextureRegistry, FlutterPluginRegistry>
@@ -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.

View File

@@ -36,12 +36,18 @@ FLUTTER_DARWIN_EXPORT
@property(nonnull, readonly) id<FlutterTextureRegistry> 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;
- (nullable 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|.

View File

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

View File

@@ -19,7 +19,7 @@ bool FlutterCompositor::CreateBackingStore(const FlutterBackingStoreConfig* conf
// 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::CreateBackingStore(const FlutterBackingStoreConfig* conf
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;
}

View File

@@ -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 <FlutterViewProvider> {
FlutterView* _defaultView;
}
@@ -30,7 +32,7 @@
return self;
}
- (nullable FlutterView*)getView:(uint64_t)viewId {
- (nullable FlutterView*)viewForId:(uint64_t)viewId {
if (viewId == kFlutterDefaultViewId) {
return _defaultView;
}

View File

@@ -84,6 +84,29 @@ constexpr char kTextPlainFormat[] = "text/plain";
- (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.
*/
@@ -161,10 +184,18 @@ constexpr char kTextPlainFormat[] = "text/plain";
}
- (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;
}
return _flutterEngine.viewController.flutterView;
if (!controller.viewLoaded) {
[controller loadView];
}
return controller.flutterView;
}
- (void)addMethodCallDelegate:(nonnull id<FlutterPlugin>)delegate
@@ -214,6 +245,11 @@ static void OnPlatformMessage(const FlutterPlatformMessage* message, FlutterEngi
// when the engine is destroyed.
std::unique_ptr<flutter::FlutterCompositor> _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 +266,8 @@ static void OnPlatformMessage(const FlutterPlatformMessage* message, FlutterEngi
// A method channel for miscellaneous platform functionality.
FlutterMethodChannel* _platformChannel;
int _nextViewId;
}
- (instancetype)initWithName:(NSString*)labelPrefix project:(FlutterDartProject*)project {
@@ -249,10 +287,14 @@ static void OnPlatformMessage(const FlutterPlatformMessage* message, FlutterEngi
_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 +326,7 @@ static void OnPlatformMessage(const FlutterPlatformMessage* message, FlutterEngi
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 +353,11 @@ static void OnPlatformMessage(const FlutterPlatformMessage* message, FlutterEngi
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 +419,14 @@ static void OnPlatformMessage(const FlutterPlatformMessage* message, FlutterEngi
}
[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 +460,59 @@ static void OnPlatformMessage(const FlutterPlatformMessage* message, FlutterEngi
}
}
- (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 +520,14 @@ static void OnPlatformMessage(const FlutterPlatformMessage* message, FlutterEngi
@"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<flutter::FlutterCompositor>(
[[FlutterViewEngineProvider alloc] initWithEngine:self], _platformViewController);
@@ -485,6 +572,17 @@ static void OnPlatformMessage(const FlutterPlatformMessage* message, FlutterEngi
#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 +642,19 @@ static void OnPlatformMessage(const FlutterPlatformMessage* message, FlutterEngi
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 +685,14 @@ static void OnPlatformMessage(const FlutterPlatformMessage* message, FlutterEngi
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 +708,6 @@ static void OnPlatformMessage(const FlutterPlatformMessage* message, FlutterEngi
#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 +771,10 @@ static void OnPlatformMessage(const FlutterPlatformMessage* message, FlutterEngi
}
- (void)engineCallbackOnPreEngineRestart {
if (_viewController) {
[_viewController onPreEngineRestart];
NSEnumerator* viewControllerEnumerator = [_viewControllers objectEnumerator];
FlutterViewController* nextViewController;
while ((nextViewController = [viewControllerEnumerator nextObject])) {
[nextViewController onPreEngineRestart];
}
}
@@ -682,7 +786,11 @@ static void OnPlatformMessage(const FlutterPlatformMessage* message, FlutterEngi
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 +855,12 @@ static void OnPlatformMessage(const FlutterPlatformMessage* message, FlutterEngi
- (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;
}

View File

@@ -614,6 +614,69 @@ TEST(EngineTest, ThreadSynchronizerNotBlockingRasterThreadAfterShutdown) {
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)

View File

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

View File

@@ -57,7 +57,7 @@ typedef _Nullable _NSResponderPtr (^NextResponderProvider)();
/**
* The text input plugin set by initialization.
*/
@property(nonatomic) id<FlutterKeyboardViewDelegate> viewDelegate;
@property(nonatomic, weak) id<FlutterKeyboardViewDelegate> viewDelegate;
/**
* The primary responders added by addPrimaryResponder.

View File

@@ -89,7 +89,7 @@ static bool OnAcquireExternalTexture(FlutterEngine* engine,
#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 @@ static bool OnAcquireExternalTexture(FlutterEngine* engine,
}
- (BOOL)present:(uint64_t)viewId texture:(const FlutterMetalTexture*)texture {
FlutterView* view = [_viewProvider getView:viewId];
FlutterView* view = [_viewProvider viewForId:viewId];
if (view == nil) {
return NO;
}

View File

@@ -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<MTLDevice>)device
commandQueue:(id<MTLCommandQueue>)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 @@ TEST(FlutterRenderer, PresentDelegatesToFlutterView) {
}
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);

View File

@@ -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 @@ static void CommonInit(FlutterViewController* controller, 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 @@ static void CommonInit(FlutterViewController* controller, FlutterEngine* engine)
}
- (void)dealloc {
_engine.viewController = nil;
if ([self attached]) {
[_engine removeViewController:self];
}
CFNotificationCenterRef cfCenter = CFNotificationCenterGetDistributedCenter();
CFNotificationCenterRemoveEveryObserver(cfCenter, (__bridge void*)self);
}
@@ -583,8 +581,7 @@ static void CommonInit(FlutterViewController* controller, FlutterEngine* engine)
- (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 @@ static void CommonInit(FlutterViewController* controller, FlutterEngine* engine)
* 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

View File

@@ -22,14 +22,8 @@
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

View File

@@ -6,6 +6,7 @@
#import <Foundation/Foundation.h>
#import <OCMock/OCMock.h>
#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 @@ namespace flutter::testing {
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

View File

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