@@ -505,6 +505,9 @@ @implementation FlutterTextInputView {
505505 const char * _selectionAffinity;
506506 FlutterTextRange* _selectedTextRange;
507507 CGRect _cachedFirstRect;
508+ // The view has reached end of life, and is no longer
509+ // allowed to access its textInputDelegate.
510+ BOOL _decommissioned;
508511}
509512
510513@synthesize tokenizer = _tokenizer;
@@ -535,6 +538,7 @@ - (instancetype)init {
535538 _returnKeyType = UIReturnKeyDone;
536539 _secureTextEntry = NO ;
537540 _accessibilityEnabled = NO ;
541+ _decommissioned = NO ;
538542 if (@available (iOS 11.0 , *)) {
539543 _smartQuotesType = UITextSmartQuotesTypeYes;
540544 _smartDashesType = UITextSmartDashesTypeYes;
@@ -545,6 +549,7 @@ - (instancetype)init {
545549}
546550
547551- (void )configureWithDictionary : (NSDictionary *)configuration {
552+ NSAssert (!_decommissioned, @" Attempt to reuse a decommissioned view, for %@ " , configuration);
548553 NSDictionary * inputType = configuration[kKeyboardType ];
549554 NSString * keyboardAppearance = configuration[kKeyboardAppearance ];
550555 NSDictionary * autofill = configuration[kAutofillProperties ];
@@ -596,6 +601,23 @@ - (UITextContentType)textContentType {
596601 return _textContentType;
597602}
598603
604+ - (id <FlutterTextInputDelegate>)textInputDelegate {
605+ return _decommissioned ? nil : _textInputDelegate;
606+ }
607+
608+ // Declares that the view has reached end of life, and
609+ // is no longer allowed to access its textInputDelegate.
610+ //
611+ // UIKit may retain this view (even after it's been removed
612+ // from the view hierarchy) so that it may outlive the plugin/engine,
613+ // in which case _textInputDelegate will become a dangling pointer.
614+
615+ // The text input plugin needs to call decommision when it should
616+ // not have access to its FlutterTextInputDelegate any more.
617+ - (void )decommision {
618+ _decommissioned = YES ;
619+ }
620+
599621- (void )dealloc {
600622 [_text release ];
601623 [_markedText release ];
@@ -778,7 +800,8 @@ - (void)replaceRange:(UITextRange*)range withText:(NSString*)text {
778800
779801- (BOOL )shouldChangeTextInRange : (UITextRange*)range replacementText : (NSString *)text {
780802 if (self.returnKeyType == UIReturnKeyDefault && [text isEqualToString: @" \n " ]) {
781- [_textInputDelegate performAction: FlutterTextInputActionNewline withClient: _textInputClient];
803+ [self .textInputDelegate performAction: FlutterTextInputActionNewline
804+ withClient: _textInputClient];
782805 return YES ;
783806 }
784807
@@ -819,7 +842,7 @@ - (BOOL)shouldChangeTextInRange:(UITextRange*)range replacementText:(NSString*)t
819842 break ;
820843 }
821844
822- [_textInputDelegate performAction: action withClient: _textInputClient];
845+ [self .textInputDelegate performAction: action withClient: _textInputClient];
823846 return NO ;
824847 }
825848
@@ -1062,9 +1085,9 @@ - (CGRect)firstRectForRange:(UITextRange*)range {
10621085 return _cachedFirstRect;
10631086 }
10641087
1065- [_textInputDelegate showAutocorrectionPromptRectForStart: start
1066- end: end
1067- withClient: _textInputClient];
1088+ [self .textInputDelegate showAutocorrectionPromptRectForStart: start
1089+ end: end
1090+ withClient: _textInputClient];
10681091 // TODO(cbracken) Implement.
10691092 return CGRectZero;
10701093}
@@ -1097,21 +1120,21 @@ - (UITextRange*)characterRangeAtPoint:(CGPoint)point {
10971120}
10981121
10991122- (void )beginFloatingCursorAtPoint : (CGPoint)point {
1100- [_textInputDelegate updateFloatingCursor: FlutterFloatingCursorDragStateStart
1101- withClient: _textInputClient
1102- withPosition: @{@" X" : @(point.x ), @" Y" : @(point.y )}];
1123+ [self .textInputDelegate updateFloatingCursor: FlutterFloatingCursorDragStateStart
1124+ withClient: _textInputClient
1125+ withPosition: @{@" X" : @(point.x ), @" Y" : @(point.y )}];
11031126}
11041127
11051128- (void )updateFloatingCursorAtPoint : (CGPoint)point {
1106- [_textInputDelegate updateFloatingCursor: FlutterFloatingCursorDragStateUpdate
1107- withClient: _textInputClient
1108- withPosition: @{@" X" : @(point.x ), @" Y" : @(point.y )}];
1129+ [self .textInputDelegate updateFloatingCursor: FlutterFloatingCursorDragStateUpdate
1130+ withClient: _textInputClient
1131+ withPosition: @{@" X" : @(point.x ), @" Y" : @(point.y )}];
11091132}
11101133
11111134- (void )endFloatingCursor {
1112- [_textInputDelegate updateFloatingCursor: FlutterFloatingCursorDragStateEnd
1113- withClient: _textInputClient
1114- withPosition: @{@" X" : @(0 ), @" Y" : @(0 )}];
1135+ [self .textInputDelegate updateFloatingCursor: FlutterFloatingCursorDragStateEnd
1136+ withClient: _textInputClient
1137+ withPosition: @{@" X" : @(0 ), @" Y" : @(0 )}];
11151138}
11161139
11171140#pragma mark - UIKeyInput Overrides
@@ -1139,9 +1162,11 @@ - (void)updateEditingState {
11391162 };
11401163
11411164 if (_textInputClient == 0 && _autofillId != nil ) {
1142- [_textInputDelegate updateEditingClient: _textInputClient withState: state withTag: _autofillId];
1165+ [self .textInputDelegate updateEditingClient: _textInputClient
1166+ withState: state
1167+ withTag: _autofillId];
11431168 } else {
1144- [_textInputDelegate updateEditingClient: _textInputClient withState: state];
1169+ [self .textInputDelegate updateEditingClient: _textInputClient withState: state];
11451170 }
11461171}
11471172
@@ -1259,8 +1284,6 @@ - (void)enableActiveViewAccessibility {
12591284@end
12601285
12611286@interface FlutterTextInputPlugin ()
1262- @property (nonatomic , strong ) FlutterTextInputView* reusableInputView;
1263-
12641287// The current password-autofillable input fields that have yet to be saved.
12651288@property (nonatomic , readonly )
12661289 NSMutableDictionary <NSString*, FlutterTextInputView*>* autofillContext;
@@ -1278,11 +1301,11 @@ - (instancetype)init {
12781301 self = [super init ];
12791302
12801303 if (self) {
1281- _reusableInputView = [[FlutterTextInputView alloc ] init ];
1282- _reusableInputView.secureTextEntry = NO ;
12831304 _autofillContext = [[NSMutableDictionary alloc ] init ];
1284- _activeView = [_reusableInputView retain ];
12851305 _inputHider = [[FlutterTextInputViewAccessibilityHider alloc ] init ];
1306+ // Initialize activeView with a dummy view to keep tests
1307+ // passing.
1308+ _activeView = [[FlutterTextInputView alloc ] init ];
12861309 }
12871310
12881311 return self;
@@ -1291,7 +1314,6 @@ - (instancetype)init {
12911314- (void )dealloc {
12921315 [self hideTextInput ];
12931316 _activeView.textInputDelegate = nil ;
1294- [_reusableInputView release ];
12951317 [_activeView release ];
12961318 [_inputHider release ];
12971319 [_autofillContext release ];
@@ -1398,14 +1420,14 @@ - (void)triggerAutofillSave:(BOOL)saveEntries {
13981420 if (saveEntries) {
13991421 // Make all the input fields in the autofill context visible,
14001422 // then remove them to trigger autofill save.
1401- [self cleanUpViewHierarchy: YES clearText: YES ];
1423+ [self cleanUpViewHierarchy: YES clearText: YES delayRemoval: NO ];
14021424 [_autofillContext removeAllObjects ];
14031425 [self changeInputViewsAutofillVisibility: YES ];
14041426 } else {
14051427 [_autofillContext removeAllObjects ];
14061428 }
14071429
1408- [self cleanUpViewHierarchy: YES clearText: !saveEntries];
1430+ [self cleanUpViewHierarchy: YES clearText: !saveEntries delayRemoval: NO ];
14091431 [self addToInputParentViewIfNeeded: _activeView];
14101432}
14111433
@@ -1414,9 +1436,11 @@ - (void)setTextInputClient:(int)client withConfiguration:(NSDictionary*)configur
14141436 // Hide all input views from autofill, only make those in the new configuration visible
14151437 // to autofill.
14161438 [self changeInputViewsAutofillVisibility: NO ];
1439+
1440+ // Update the current active view.
14171441 switch (autofillTypeOf (configuration)) {
14181442 case FlutterAutofillTypeNone:
1419- self.activeView = [self updateAndShowReusableInputView : configuration];
1443+ self.activeView = [self createInputViewWith : configuration];
14201444 break ;
14211445 case FlutterAutofillTypeRegular:
14221446 // If the group does not involve password autofill, only install the
@@ -1431,7 +1455,6 @@ - (void)setTextInputClient:(int)client withConfiguration:(NSDictionary*)configur
14311455 isPasswordRelated: YES ];
14321456 break ;
14331457 }
1434-
14351458 [_activeView setTextInputClient: client];
14361459 [_activeView reloadInputViews ];
14371460
@@ -1441,35 +1464,37 @@ - (void)setTextInputClient:(int)client withConfiguration:(NSDictionary*)configur
14411464 // them to free up resources and reduce the number of input views in the view
14421465 // hierarchy.
14431466 //
1444- // This is scheduled on the runloop and delayed by 0.1s so we don't remove the
1467+ // The garbage views are decommissioned immediately, but the removeFromSuperview
1468+ // call is scheduled on the runloop and delayed by 0.1s so we don't remove the
14451469 // text fields immediately (which seems to make the keyboard flicker).
14461470 // See: https://github.com/flutter/flutter/issues/64628.
1447- [self performSelector: @selector ( collectGarbageInputViews ) withObject: nil afterDelay: 0.1 ];
1471+ [self cleanUpViewHierarchy: NO clearText: YES delayRemoval: YES ];
14481472}
14491473
1450- // Updates and shows an input field that is not password related and has no autofill
1451- // hints. This method re-configures and reuses an existing instance of input field
1452- // instead of creating a new one.
1453- // Also updates the current autofill context.
1454- - (FlutterTextInputView*)updateAndShowReusableInputView : (NSDictionary *)configuration {
1474+ // Creates and shows an input field that is not password related and has no autofill
1475+ // hints. This method returns a new FlutterTextInputView instance when called, since
1476+ // UIKit uses the identity of `UITextInput` instances (or the identity of the input
1477+ // views) to decide whether the IME's internal states should be reset. See:
1478+ // https://github.com/flutter/flutter/issues/79031 .
1479+ - (FlutterTextInputView*)createInputViewWith : (NSDictionary *)configuration {
14551480 // It's possible that the configuration of this non-autofillable input view has
14561481 // an autofill configuration without hints. If it does, remove it from the context.
14571482 NSString * autofillId = autofillIdFromDictionary (configuration);
14581483 if (autofillId) {
14591484 [_autofillContext removeObjectForKey: autofillId];
14601485 }
1461-
1462- [_reusableInputView configureWithDictionary: configuration];
1463- [self addToInputParentViewIfNeeded: _reusableInputView ];
1464- _reusableInputView .textInputDelegate = _textInputDelegate;
1486+ FlutterTextInputView* newView = [[FlutterTextInputView alloc ] init ];
1487+ [newView configureWithDictionary: configuration];
1488+ [self addToInputParentViewIfNeeded: newView ];
1489+ newView .textInputDelegate = _textInputDelegate;
14651490
14661491 for (NSDictionary * field in configuration[kAssociatedAutofillFields ]) {
14671492 NSString * autofillId = autofillIdFromDictionary (field);
14681493 if (autofillId && autofillTypeOf (field) == FlutterAutofillTypeNone) {
14691494 [_autofillContext removeObjectForKey: autofillId];
14701495 }
14711496 }
1472- return _reusableInputView ;
1497+ return [newView autorelease ] ;
14731498}
14741499
14751500- (FlutterTextInputView*)updateAndShowAutofillViews : (NSArray *)fields
@@ -1547,11 +1572,21 @@ - (UIView*)keyWindow {
15471572 return _inputHider.subviews ;
15481573}
15491574
1550- // Removes every installed input field, unless it's in the current autofill
1551- // context. May remove the active view too if includeActiveView is YES.
1575+ // Decommisions (See the "decommision" method on FlutterTextInputView) and removes
1576+ // every installed input field, unless it's in the current autofill context.
1577+ //
1578+ // The active view will be decommisioned and removed from its superview too, if
1579+ // includeActiveView is YES.
15521580// When clearText is YES, the text on the input fields will be set to empty before
15531581// they are removed from the view hierarchy, to avoid triggering autofill save.
1554- - (void )cleanUpViewHierarchy : (BOOL )includeActiveView clearText : (BOOL )clearText {
1582+ // If delayRemoval is true, removeFromSuperview will be scheduled on the runloop and
1583+ // will be delayed by 0.1s so we don't remove the text fields immediately (which seems
1584+ // to make the keyboard flicker).
1585+ // See: https://github.com/flutter/flutter/issues/64628.
1586+
1587+ - (void )cleanUpViewHierarchy : (BOOL )includeActiveView
1588+ clearText : (BOOL )clearText
1589+ delayRemoval : (BOOL )delayRemoval {
15551590 for (UIView* view in self.textInputViews ) {
15561591 if ([view isKindOfClass: [FlutterTextInputView class ]] &&
15571592 (includeActiveView || view != _activeView)) {
@@ -1560,16 +1595,17 @@ - (void)cleanUpViewHierarchy:(BOOL)includeActiveView clearText:(BOOL)clearText {
15601595 if (clearText) {
15611596 [inputView replaceRangeLocal: NSMakeRange (0 , inputView.text.length) withText: @" " ];
15621597 }
1563- [view removeFromSuperview ];
1598+ [inputView decommision ];
1599+ if (delayRemoval) {
1600+ [inputView performSelector: @selector (removeFromSuperview ) withObject: nil afterDelay: 0.1 ];
1601+ } else {
1602+ [inputView removeFromSuperview ];
1603+ }
15641604 }
15651605 }
15661606 }
15671607}
15681608
1569- - (void )collectGarbageInputViews {
1570- [self cleanUpViewHierarchy: NO clearText: YES ];
1571- }
1572-
15731609// Changes the visibility of every FlutterTextInputView currently in the
15741610// view hierarchy.
15751611- (void )changeInputViewsAutofillVisibility : (BOOL )newVisibility {
@@ -1582,7 +1618,12 @@ - (void)changeInputViewsAutofillVisibility:(BOOL)newVisibility {
15821618}
15831619
15841620// Resets the client id of every FlutterTextInputView in the view hierarchy
1585- // to 0. Called when a new text input connection will be established.
1621+ // to 0.
1622+ // Called before establishing a new text input connection.
1623+ // For views in the current autofill context, they need to
1624+ // stay in the view hierachy but should not be allowed to
1625+ // send messages (other than autofill related ones) to the
1626+ // framework.
15861627- (void )resetAllClientIds {
15871628 for (UIView* view in self.textInputViews ) {
15881629 if ([view isKindOfClass: [FlutterTextInputView class ]]) {
0 commit comments