Skip to content
This repository was archived by the owner on Feb 25, 2025. It is now read-only.

Conversation

@LongCatIsLooong
Copy link
Contributor

@LongCatIsLooong LongCatIsLooong commented May 28, 2020

part of flutter/flutter#55613
framework PR: flutter/flutter#58731

Android

Android saves the autofillable input fields by value under the hood. We just have to call AutofillManager#commit or AutofillManager#commit at the right time.

iOS

iOS does some accounting to keep track of possible password autofill fields (but it does not save the input by value, instead it keeps track of the references to the input views).
When an input view is being removed from the view hierarchy, iOS uses heuristics to determine if the view is password related and will try to extract & save the username/password.

For iOS, I'm keeping the password-related input fields in the view hierarchy until we're told to start a new autofill context (TextInput.AutofillContext.{commit, cancel}).

iOS demo

The username & password fields (under the "login" section) initially didn't have any autofill candidates. After using automatic strong password to generate a new password (the "sign up" section) and tapping submit to save the new username/password pairs, a new autofill entry is added to the quick type bar when revisiting the username/password fields.

ios-pw-save

// that adopts the UITextInput protocol, automatic strong password seems to
// currently only support UITextFields, and password saving only supports
// UITextFields and UITextViews, as of iOS 13.5.
@interface FlutterSecureTextInputView : FlutterTextInputView
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changing FlutterTextInputView to a UITextField seems to require a lot of code changes so I chose this hack instead. From the documentation I think not supporting custom UITextInput is a bug in UIKit (unfortunately it happens on both iOS 12 & iOS 13), once it's fixed we can get rid of this class.

Copy link
Contributor Author

@LongCatIsLooong LongCatIsLooong May 28, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unrecognized selectors need to be forwarded to the shared text field instance because UIKit's keyboard implementation calls private UITextField methods (we tricked it into thinking this is a UITextField instance).

Copy link
Contributor

@justinmc justinmc left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM from my point of view, but definitely wait for the other reviewers to take a look as well. The FlutterSecureTextInputView hack seems reasonable to me for now.

@nturgut
Copy link
Contributor

nturgut commented May 28, 2020

I'll test this locally with web engine changes and let you know. It might take a day, sorry for the inconvenience.

Copy link
Member

@gaaclarke gaaclarke left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed iOS portion. It's an intense PR but great work. I just had one legitimate concern regarding the static input field.

}

static NSString* uniqueIdFromDictionary(NSDictionary* dictionary) {
static NSString* autofillIdFromDictionary(NSDictionary* dictionary) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One sentence docstring would be nice.

+ (UITextField*)textField {
static UITextField* _textField = nil;
if (_textField == nil) {
_textField = [[UITextField alloc] init];
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why not have a UITextField per FlutterSecureTextInputView? This seems a bit risky considering we are forwarding unknown messages all to a singular instance.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The unrecognized selectors sent to FlutterSecureTextInputView that I'm seeing in my demo app:

rightView
leftView
textAlignment
font
_setContentCoverViewMode:
_setContentCoverView:
_setBackgroundCoverViewMode:
_setBackgroundCoverView:
_layoutManager

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the assumption here is that the UITextField instance is just a stateless (for the states that matter to us) selector sponge. For the selectors I'm seeing the hack seems to work fine. If this hack doesn't work then we should probably switch to a completely different approach (e.g., subclassing UITextField).
If there are other selectors that may set the UITextField to a broken state, then having separate instances probably won't prevent that from happening? If we make each FlutterSecureTextInputView use its own UITextField, we are going to reuse the UITextField anyway when we reuse the FlutterSecureTextInputView.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It isn't stateless though since we are forwarding messages to it that could be setting all kinds of state.

[inputView1 setAccessibilityHint:@"hey"];
print(inputView2.accessibilityHint);

for example

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated to create a UITextField per instance. What I meant was if any of the states those unrecognized selectors can change matter to us or iOS itself, then using this hack is pretty much a lost cause.

if (!autofillIdFromDictionary(configuration)) {
return NO;
}
NSDictionary* autofill = configuration[@"autofill"];
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For all these string literals that are shared across the code it would be nice to have const variables for them to avoid errors.
static NSString *const APIPathFoo = APIBasePath @"foo";

}

- (void)setIsVisibleToAutofill:(BOOL)isVisibleToAutofill {
self.frame = isVisibleToAutofill ? CGRectMake(0, 0, 1, 1) : CGRectZero;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This might merit a comment since it seems a bit hacky to change the frame based on something like if it is visible to autofill.

@property(nonatomic, retain) FlutterTextInputView* nonAutofillInputView;
@property(nonatomic, retain) FlutterTextInputView* nonAutofillSecureInputView;
@property(nonatomic, retain) NSMutableArray<FlutterTextInputView*>* inputViews;
@property(nonatomic, retain) FlutterTextInputView* reusableInputView;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: @property(nonatomic, strong)

NSDictionary* autofill = configuration[@"autofill"];
inputView.textInputDelegate = _textInputDelegate;
[inputView configureWithDictionary:field];
return inputView;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

super nit: move the autorelease to the return statement to make auditing a bit easier

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the inputView is not necessarily a new object, putting autorelease at the end would make it a zombie when it's reused.

static const char _kTextAffinityUpstream[] = "TextAffinity.upstream";

#pragma mark - TextInputConfiguration Field Names
static NSString* const _kSecureTextEntry = @"obscureText";
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You shouldn't have the _ prefix here: https://google.github.io/styleguide/objcguide.html#constants

@LongCatIsLooong
Copy link
Contributor Author

There's a crash in objc_msgSend (related to the UITextField I guess). Fixing. Will keep you posted.

@LongCatIsLooong
Copy link
Contributor Author

The crash should be fixed now.

Copy link
Member

@gaaclarke gaaclarke left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

@end

@implementation FlutterSecureTextInputView {
UITextField* _textField;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: This isn't necessary since the property will declare it.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this is required because the property is readOnly and I overrode its getter so it will not get synthesized?

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants