diff --git a/shell/platform/darwin/ios/framework/Source/FlutterEngine.mm b/shell/platform/darwin/ios/framework/Source/FlutterEngine.mm index 10fe77d6a217f..b2374fa0d147c 100644 --- a/shell/platform/darwin/ios/framework/Source/FlutterEngine.mm +++ b/shell/platform/darwin/ios/framework/Source/FlutterEngine.mm @@ -33,6 +33,7 @@ @interface FlutterEngine () @property(nonatomic, readonly) NSMutableDictionary* pluginPublications; @property(nonatomic, readwrite, copy) NSString* isolateId; +@property(nonatomic, retain) id flutterViewControllerWillDeallocObserver; @end @interface FlutterEngineRegistrar : NSObject @@ -111,6 +112,7 @@ - (void)dealloc { NSNotificationCenter* center = [NSNotificationCenter defaultCenter]; [center removeObserver:self name:UIApplicationDidReceiveMemoryWarningNotification object:nil]; + [_flutterViewControllerWillDeallocObserver release]; [super dealloc]; } @@ -167,12 +169,21 @@ - (void)setViewController:(FlutterViewController*)viewController { _viewController = [viewController getWeakPtr]; self.iosPlatformView->SetOwnerViewController(_viewController); [self maybeSetupPlatformViewChannels]; + + self.flutterViewControllerWillDeallocObserver = + [[NSNotificationCenter defaultCenter] addObserverForName:FlutterViewControllerWillDealloc + object:viewController + queue:[NSOperationQueue mainQueue] + usingBlock:^(NSNotification* note) { + [self notifyViewControllerDeallocated]; + }]; } - (void)notifyViewControllerDeallocated { if (!_allowHeadlessExecution) { [self destroyContext]; } + _viewController.reset(); } - (void)destroyContext { diff --git a/shell/platform/darwin/ios/framework/Source/FlutterEngine_Internal.h b/shell/platform/darwin/ios/framework/Source/FlutterEngine_Internal.h index 868d2176a309b..573333ea732e6 100644 --- a/shell/platform/darwin/ios/framework/Source/FlutterEngine_Internal.h +++ b/shell/platform/darwin/ios/framework/Source/FlutterEngine_Internal.h @@ -44,7 +44,6 @@ - (FlutterTextInputPlugin*)textInputPlugin; - (void)launchEngine:(NSString*)entrypoint libraryURI:(NSString*)libraryOrNil; - (BOOL)createShell:(NSString*)entrypoint libraryURI:(NSString*)libraryOrNil; -- (void)notifyViewControllerDeallocated; @end diff --git a/shell/platform/darwin/ios/framework/Source/FlutterViewController.mm b/shell/platform/darwin/ios/framework/Source/FlutterViewController.mm index 3cea602f86bad..ab7564d2d7132 100644 --- a/shell/platform/darwin/ios/framework/Source/FlutterViewController.mm +++ b/shell/platform/darwin/ios/framework/Source/FlutterViewController.mm @@ -25,6 +25,8 @@ NSNotificationName const FlutterSemanticsUpdateNotification = @"FlutterSemanticsUpdate"; +NSNotificationName const FlutterViewControllerWillDealloc = @"FlutterViewControllerWillDealloc"; + // This is left a FlutterBinaryMessenger privately for now to give people a chance to notice the // change. Unfortunately unless you have Werror turned on, incompatible pointers as arguments are // just a warning. @@ -521,7 +523,9 @@ - (void)flushOngoingTouches { } - (void)dealloc { - [_engine.get() notifyViewControllerDeallocated]; + [[NSNotificationCenter defaultCenter] postNotificationName:FlutterViewControllerWillDealloc + object:self + userInfo:nil]; [[NSNotificationCenter defaultCenter] removeObserver:self]; [_ongoingTouches release]; [super dealloc]; diff --git a/shell/platform/darwin/ios/framework/Source/FlutterViewControllerTest.m b/shell/platform/darwin/ios/framework/Source/FlutterViewControllerTest.m index ca431da779094..22cdeb3d676c0 100644 --- a/shell/platform/darwin/ios/framework/Source/FlutterViewControllerTest.m +++ b/shell/platform/darwin/ios/framework/Source/FlutterViewControllerTest.m @@ -12,6 +12,23 @@ #error ARC must be enabled! #endif +extern NSNotificationName const FlutterViewControllerWillDealloc; + +/// A simple mock class for FlutterEngine. +/// +/// OCMockClass can't be used for FlutterEngine sometimes because OCMock retains arguments to +/// invocations and since the init for FlutterViewController calls a method on the +/// FlutterEngine it creates a retain cycle that stops us from testing behaviors related to +/// deleting FlutterViewControllers. +@interface MockEngine : NSObject +@end + +@implementation MockEngine +- (void)setViewController:(FlutterViewController*)viewController { + // noop +} +@end + @interface FlutterViewControllerTest : XCTestCase @end @@ -244,4 +261,22 @@ - (UITraitCollection*)fakeTraitCollectionWithContrast:(UIAccessibilityContrast)c return mockTraitCollection; } +- (void)testWillDeallocNotification { + XCTestExpectation* expectation = + [[XCTestExpectation alloc] initWithDescription:@"notification called"]; + id engine = [[MockEngine alloc] init]; + FlutterViewController* realVC = [[FlutterViewController alloc] initWithEngine:engine + nibName:nil + bundle:nil]; + id observer = + [[NSNotificationCenter defaultCenter] addObserverForName:FlutterViewControllerWillDealloc + object:nil + queue:[NSOperationQueue mainQueue] + usingBlock:^(NSNotification* _Nonnull note) { + [expectation fulfill]; + }]; + realVC = nil; + [self waitForExpectations:@[ expectation ] timeout:1.0]; +} + @end diff --git a/shell/platform/darwin/ios/framework/Source/FlutterViewController_Internal.h b/shell/platform/darwin/ios/framework/Source/FlutterViewController_Internal.h index 5947a9327ff3d..afe5b8166ce4a 100644 --- a/shell/platform/darwin/ios/framework/Source/FlutterViewController_Internal.h +++ b/shell/platform/darwin/ios/framework/Source/FlutterViewController_Internal.h @@ -12,6 +12,9 @@ #include "flutter/shell/platform/darwin/ios/framework/Headers/FlutterViewController.h" #include "flutter/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews_Internal.h" +FLUTTER_EXPORT +extern NSNotificationName const FlutterViewControllerWillDealloc; + @interface FlutterViewController () - (fml::WeakPtr)getWeakPtr; diff --git a/shell/platform/darwin/ios/platform_view_ios.h b/shell/platform/darwin/ios/platform_view_ios.h index 38fc2a9f30b28..a8574350d010c 100644 --- a/shell/platform/darwin/ios/platform_view_ios.h +++ b/shell/platform/darwin/ios/platform_view_ios.h @@ -52,6 +52,7 @@ class PlatformViewIOS final : public PlatformView { std::unique_ptr accessibility_bridge_; fml::scoped_nsprotocol text_input_plugin_; fml::closure firstFrameCallback_; + fml::scoped_nsprotocol dealloc_view_controller_observer_; // |PlatformView| void HandlePlatformMessage(fml::RefPtr message) override; diff --git a/shell/platform/darwin/ios/platform_view_ios.mm b/shell/platform/darwin/ios/platform_view_ios.mm index c80ae377c03a1..d42b3adf2d153 100644 --- a/shell/platform/darwin/ios/platform_view_ios.mm +++ b/shell/platform/darwin/ios/platform_view_ios.mm @@ -49,6 +49,19 @@ accessibility_bridge_.reset(); } owner_controller_ = owner_controller; + + // Add an observer that will clear out the owner_controller_ ivar and + // the accessibility_bridge_ in case the view controller is deleted. + dealloc_view_controller_observer_.reset([[NSNotificationCenter defaultCenter] + addObserverForName:FlutterViewControllerWillDealloc + object:owner_controller_.get() + queue:[NSOperationQueue mainQueue] + usingBlock:^(NSNotification* note) { + // Implicit copy of 'this' is fine. + accessibility_bridge_.reset(); + owner_controller_.reset(); + }]); + if (owner_controller_) { ios_surface_ = [static_cast(owner_controller.get().view) createSurface:gl_context_];