diff --git a/engine/src/flutter/shell/platform/darwin/ios/framework/Source/FlutterUndoManagerPlugin.mm b/engine/src/flutter/shell/platform/darwin/ios/framework/Source/FlutterUndoManagerPlugin.mm index 6eeb106c16..32810dcacc 100644 --- a/engine/src/flutter/shell/platform/darwin/ios/framework/Source/FlutterUndoManagerPlugin.mm +++ b/engine/src/flutter/shell/platform/darwin/ios/framework/Source/FlutterUndoManagerPlugin.mm @@ -13,6 +13,10 @@ static NSString* const kCanRedo = @"canRedo"; @interface FlutterUndoManagerPlugin () @property(nonatomic, weak, readonly) id undoManagerDelegate; + +// When the delegate is `FlutterEngine` this will be the `FlutterViewController`'s undo manager. +// Strongly retain to ensure this target's actions are completely removed during dealloc. +@property(nonatomic) NSUndoManager* undoManager; @end @implementation FlutterUndoManagerPlugin @@ -28,13 +32,14 @@ static NSString* const kCanRedo = @"canRedo"; } - (void)dealloc { - [_undoManagerDelegate.undoManager removeAllActionsWithTarget:self]; + [_undoManager removeAllActionsWithTarget:self]; } - (void)handleMethodCall:(FlutterMethodCall*)call result:(FlutterResult)result { NSString* method = call.method; id args = call.arguments; if ([method isEqualToString:kSetUndoStateMethod]) { + self.undoManager = self.undoManagerDelegate.undoManager; [self setUndoState:args]; result(nil); } else { @@ -43,11 +48,11 @@ static NSString* const kCanRedo = @"canRedo"; } - (void)resetUndoManager { - [self.undoManagerDelegate.undoManager removeAllActionsWithTarget:self]; + [self.undoManager removeAllActionsWithTarget:self]; } - (void)registerUndoWithDirection:(FlutterUndoRedoDirection)direction { - NSUndoManager* undoManager = self.undoManagerDelegate.undoManager; + NSUndoManager* undoManager = self.undoManager; [undoManager beginUndoGrouping]; [undoManager registerUndoWithTarget:self handler:^(FlutterUndoManagerPlugin* target) { @@ -64,7 +69,7 @@ static NSString* const kCanRedo = @"canRedo"; } - (void)registerRedo { - NSUndoManager* undoManager = self.undoManagerDelegate.undoManager; + NSUndoManager* undoManager = self.undoManager; [undoManager beginUndoGrouping]; [undoManager registerUndoWithTarget:self handler:^(id target) { @@ -76,7 +81,7 @@ static NSString* const kCanRedo = @"canRedo"; } - (void)setUndoState:(NSDictionary*)dictionary { - NSUndoManager* undoManager = self.undoManagerDelegate.undoManager; + NSUndoManager* undoManager = self.undoManager; BOOL groupsByEvent = undoManager.groupsByEvent; undoManager.groupsByEvent = NO; BOOL canUndo = [dictionary[kCanUndo] boolValue]; diff --git a/engine/src/flutter/shell/platform/darwin/ios/framework/Source/FlutterUndoManagerPluginTest.mm b/engine/src/flutter/shell/platform/darwin/ios/framework/Source/FlutterUndoManagerPluginTest.mm index 62dcb8089a..d5d445ca62 100644 --- a/engine/src/flutter/shell/platform/darwin/ios/framework/Source/FlutterUndoManagerPluginTest.mm +++ b/engine/src/flutter/shell/platform/darwin/ios/framework/Source/FlutterUndoManagerPluginTest.mm @@ -27,6 +27,7 @@ FLUTTER_ASSERT_ARC @property(readonly) NSUInteger undoCount; @property(readonly) NSUInteger redoCount; +@property(nonatomic, nullable) NSUndoManager* undoManager; - (instancetype)initWithUndoManager:(NSUndoManager*)undoManager activeTextInputView:(TextInputViewTest*)activeTextInputView; @@ -164,4 +165,38 @@ FLUTTER_ASSERT_ARC OCMVerify(never(), [self.activeTextInputView inputDelegate]); } +- (void)testDeallocRemovesAllUndoManagerActions { + __weak FlutterUndoManagerPlugin* weakUndoManagerPlugin; + // Use a real undo manager. + NSUndoManager* undoManager = [[NSUndoManager alloc] init]; + @autoreleasepool { + id activeTextInputView = OCMClassMock([TextInputViewTest class]); + + FakeFlutterUndoManagerDelegate* undoManagerDelegate = + [[FakeFlutterUndoManagerDelegate alloc] initWithUndoManager:undoManager + activeTextInputView:activeTextInputView]; + + FlutterUndoManagerPlugin* undoManagerPlugin = + [[FlutterUndoManagerPlugin alloc] initWithDelegate:undoManagerDelegate]; + weakUndoManagerPlugin = undoManagerPlugin; + + FlutterMethodCall* setUndoStateCall = + [FlutterMethodCall methodCallWithMethodName:@"UndoManager.setUndoState" + arguments:@{@"canUndo" : @YES, @"canRedo" : @YES}]; + [undoManagerPlugin handleMethodCall:setUndoStateCall + result:^(id _Nullable result){ + }]; + XCTAssertTrue(undoManager.canUndo); + XCTAssertTrue(undoManager.canRedo); + // Fake out the undoManager being nil, which happens when the FlutterViewController deallocs and + // the undo manager can't be fetched from the FlutterEngine delegate. + undoManagerDelegate.undoManager = nil; + } + XCTAssertNil(weakUndoManagerPlugin); + // Regression test for https://github.com/flutter/flutter/issues/150408. + // Undo manager undo and redo stack should be empty after the plugin deallocs. + XCTAssertFalse(undoManager.canUndo); + XCTAssertFalse(undoManager.canRedo); +} + @end