Skip to content
This repository was archived by the owner on Feb 25, 2025. It is now read-only.
Merged
5 changes: 5 additions & 0 deletions shell/platform/darwin/ios/framework/Source/FlutterEngine.mm
Original file line number Diff line number Diff line change
Expand Up @@ -534,6 +534,11 @@ - (void)updateEditingClient:(int)client withState:(NSDictionary*)state {
arguments:@[ @(client), state ]];
}

- (void)updateEditingClient:(int)client withState:(NSDictionary*)state withTag:(NSString*)tag {
[_textInputChannel.get() invokeMethod:@"TextInputClient.updateEditingStateWithTag"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This would be safer if you used performSelector:withObject:withObject: https://developer.apple.com/documentation/objectivec/1418956-nsobject/1418667-performselector?language=objc

That will give you static analysis that the method you are invoking exists.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I think @"TextInputClient.updateEditingStateWithTag" does not represent an objective-c selector but a platform message (that will be received and handled by some dart code)?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

sorry, you're right.

arguments:@[ @(client), @{tag : state} ]];
}

- (void)updateFloatingCursor:(FlutterFloatingCursorDragState)state
withClient:(int)client
withPosition:(NSDictionary*)position {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ typedef NS_ENUM(NSInteger, FlutterFloatingCursorDragState) {
@protocol FlutterTextInputDelegate <NSObject>

- (void)updateEditingClient:(int)client withState:(NSDictionary*)state;
- (void)updateEditingClient:(int)client withState:(NSDictionary*)state withTag:(NSString*)tag;
- (void)performAction:(FlutterTextInputAction)action withClient:(int)client;
- (void)updateFloatingCursor:(FlutterFloatingCursorDragState)state
withClient:(int)client
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@
#include "flutter/shell/platform/darwin/common/framework/Headers/FlutterChannels.h"
#include "flutter/shell/platform/darwin/ios/framework/Source/FlutterTextInputDelegate.h"

#if FLUTTER_RUNTIME_MODE == FLUTTER_RUNTIME_MODE_DEBUG

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

FYI: This will be unnecessary after #17624 lands

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I was talking about the "FLUTTER_EXPORT" not being needed once that lands. We used to have to export things we wanted to tests, now tests live with the code so it isn't necessary.

FLUTTER_EXPORT
#endif
@interface FlutterTextInputPlugin : NSObject

@property(nonatomic, assign) id<FlutterTextInputDelegate> textInputDelegate;
Expand Down Expand Up @@ -71,6 +74,7 @@ FLUTTER_EXPORT
@property(nonatomic, getter=isSecureTextEntry) BOOL secureTextEntry;
@property(nonatomic) UITextSmartQuotesType smartQuotesType API_AVAILABLE(ios(11.0));
@property(nonatomic) UITextSmartDashesType smartDashesType API_AVAILABLE(ios(11.0));
@property(nonatomic, copy) UITextContentType textContentType API_AVAILABLE(ios(10.0));

@property(nonatomic, assign) id<FlutterTextInputDelegate> textInputDelegate;

Expand Down
245 changes: 203 additions & 42 deletions shell/platform/darwin/ios/framework/Source/FlutterTextInputPlugin.mm
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,97 @@ static UIReturnKeyType ToUIReturnKeyType(NSString* inputType) {
return UIReturnKeyDefault;
}

static UITextContentType ToUITextContentType(NSArray<NSString*>* hints) {
if (hints == nil || hints.count == 0)
return @"";

NSString* hint = hints[0];
if (@available(iOS 10.0, *)) {
if ([hint isEqualToString:@"addressCityAndState"])
return UITextContentTypeAddressCityAndState;

if ([hint isEqualToString:@"addressState"])
return UITextContentTypeAddressState;

if ([hint isEqualToString:@"addressCity"])
return UITextContentTypeAddressCity;

if ([hint isEqualToString:@"sublocality"])
return UITextContentTypeSublocality;

if ([hint isEqualToString:@"streetAddressLine1"])
return UITextContentTypeStreetAddressLine1;

if ([hint isEqualToString:@"streetAddressLine2"])
return UITextContentTypeStreetAddressLine2;

if ([hint isEqualToString:@"countryName"])
return UITextContentTypeCountryName;

if ([hint isEqualToString:@"fullStreetAddress"])
return UITextContentTypeFullStreetAddress;

if ([hint isEqualToString:@"postalCode"])
return UITextContentTypePostalCode;

if ([hint isEqualToString:@"location"])
return UITextContentTypeLocation;

if ([hint isEqualToString:@"creditCardNumber"])
return UITextContentTypeCreditCardNumber;

if ([hint isEqualToString:@"email"])
return UITextContentTypeEmailAddress;

if ([hint isEqualToString:@"jobTitle"])
return UITextContentTypeJobTitle;

if ([hint isEqualToString:@"givenName"])
return UITextContentTypeGivenName;

if ([hint isEqualToString:@"middleName"])
return UITextContentTypeMiddleName;

if ([hint isEqualToString:@"familyName"])
return UITextContentTypeFamilyName;

if ([hint isEqualToString:@"name"])
return UITextContentTypeName;

if ([hint isEqualToString:@"namePrefix"])
return UITextContentTypeNamePrefix;

if ([hint isEqualToString:@"nameSuffix"])
return UITextContentTypeNameSuffix;

if ([hint isEqualToString:@"nickname"])
return UITextContentTypeNickname;

if ([hint isEqualToString:@"organizationName"])
return UITextContentTypeOrganizationName;

if ([hint isEqualToString:@"telephoneNumber"])
return UITextContentTypeTelephoneNumber;
}

if (@available(iOS 11.0, *)) {
if ([hint isEqualToString:@"password"])
return UITextContentTypePassword;
}

if (@available(iOS 12.0, *)) {
if ([hint isEqualToString:@"oneTimeCode"])
return UITextContentTypeOneTimeCode;
}

return hints[0];
}

static NSString* _uniqueIdFromDictionary(NSDictionary* dictionary) {
NSDictionary* autofill = dictionary[@"autofill"];
return autofill == nil ? nil : autofill[@"uniqueIdentifier"];
}

#pragma mark - FlutterTextPosition

@implementation FlutterTextPosition
Expand Down Expand Up @@ -141,6 +232,7 @@ - (id)copyWithZone:(NSZone*)zone {
@end

@implementation FlutterTextInputView {
NSString* _autofillId;
int _textInputClient;
const char* _selectionAffinity;
FlutterTextRange* _selectedTextRange;
Expand Down Expand Up @@ -184,13 +276,18 @@ - (void)dealloc {
[_markedTextRange release];
[_selectedTextRange release];
[_tokenizer release];
[_autofillId release];
[super dealloc];
}

- (void)setTextInputClient:(int)client {
_textInputClient = client;
}

- (void)setAutofillId:(NSString*)autofillId {
_autofillId = [autofillId copy];

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This doesn't release the old value. Instead of writing this you can just do@property(nonatomic, copy) NSString* autofillId to have one automatically generated. Put it in the .mm file to make it private.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Thank you for the review! I have to put the interface in the header file in order to be able to import that in a unit test (at least for now). Does adding autorelease after copy work here too?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Oh your pull request is already merged. I guess that means I can move the interface back to the .mm file?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Yep, autorelease can be used on any object.

}

- (void)setTextInputState:(NSDictionary*)state {
NSString* newText = state[@"text"];
BOOL textChanged = ![self.text isEqualToString:newText];
Expand Down Expand Up @@ -241,7 +338,10 @@ - (NSRange)clampSelection:(NSRange)range forText:(NSString*)text {
#pragma mark - UIResponder Overrides

- (BOOL)canBecomeFirstResponder {
return YES;
// Only the currently focused input field can
// become the first responder. This prevents iOS
// from changing focus by itself.
return _textInputClient != 0;
}

#pragma mark - UITextInput Overrides
Expand Down Expand Up @@ -600,16 +700,21 @@ - (void)updateEditingState {
composingBase = ((FlutterTextPosition*)self.markedTextRange.start).index;
composingExtent = ((FlutterTextPosition*)self.markedTextRange.end).index;
}
[_textInputDelegate updateEditingClient:_textInputClient
withState:@{
@"selectionBase" : @(selectionBase),
@"selectionExtent" : @(selectionExtent),
@"selectionAffinity" : @(_selectionAffinity),
@"selectionIsDirectional" : @(false),
@"composingBase" : @(composingBase),
@"composingExtent" : @(composingExtent),
@"text" : [NSString stringWithString:self.text],
}];

NSDictionary* state = @{
@"selectionBase" : @(selectionBase),
@"selectionExtent" : @(selectionExtent),
@"selectionAffinity" : @(_selectionAffinity),
@"selectionIsDirectional" : @(false),
@"composingBase" : @(composingBase),
@"composingExtent" : @(composingExtent),
@"text" : [NSString stringWithString:self.text],
};

if (_textInputClient == 0 && _autofillId != nil)
[_textInputDelegate updateEditingClient:_textInputClient withState:state withTag:_autofillId];
else
[_textInputDelegate updateEditingClient:_textInputClient withState:state];
}

- (BOOL)hasText {
Expand Down Expand Up @@ -672,8 +777,10 @@ - (BOOL)accessibilityElementsHidden {
@end

@implementation FlutterTextInputPlugin {
FlutterTextInputView* _view;
FlutterTextInputView* _secureView;
FlutterTextInputView* _nonAutofillInputView;
FlutterTextInputView* _nonAutofillSecureInputView;

NSMutableArray<FlutterTextInputView*>* _inputViews;
FlutterTextInputView* _activeView;
FlutterTextInputViewAccessibilityHider* _inputHider;
}
Expand All @@ -684,12 +791,13 @@ - (instancetype)init {
self = [super init];

if (self) {
_view = [[FlutterTextInputView alloc] init];
_view.secureTextEntry = NO;
_secureView = [[FlutterTextInputView alloc] init];
_secureView.secureTextEntry = YES;
_nonAutofillInputView = [[FlutterTextInputView alloc] init];
_nonAutofillInputView.secureTextEntry = NO;
_nonAutofillInputView = [[FlutterTextInputView alloc] init];
_nonAutofillSecureInputView.secureTextEntry = YES;
_inputViews = [[NSMutableArray alloc] init];

_activeView = _view;
_activeView = _nonAutofillInputView;
_inputHider = [[FlutterTextInputViewAccessibilityHider alloc] init];
}

Expand All @@ -698,9 +806,10 @@ - (instancetype)init {

- (void)dealloc {
[self hideTextInput];
[_view release];
[_secureView release];
[_nonAutofillInputView release];
[_nonAutofillSecureInputView release];
[_inputHider release];
[_inputViews release];

[super dealloc];
}
Expand Down Expand Up @@ -733,58 +842,110 @@ - (void)handleMethodCall:(FlutterMethodCall*)call result:(FlutterResult)result {
}

- (void)showTextInput {
NSAssert([UIApplication sharedApplication].keyWindow != nullptr,
UIWindow* keyWindow = [UIApplication sharedApplication].keyWindow;
NSAssert(keyWindow != nullptr,
@"The application must have a key window since the keyboard client "
@"must be part of the responder chain to function");
_activeView.textInputDelegate = _textInputDelegate;
[_inputHider addSubview:_activeView];
[[UIApplication sharedApplication].keyWindow addSubview:_inputHider];
if (![_activeView isDescendantOfView:_inputHider]) {
[_inputHider addSubview:_activeView];
}
[keyWindow addSubview:_inputHider];
[_activeView becomeFirstResponder];
}

- (void)hideTextInput {
[_activeView resignFirstResponder];
[_activeView removeFromSuperview];
[_inputHider removeFromSuperview];
}

- (void)setTextInputClient:(int)client withConfiguration:(NSDictionary*)configuration {
NSDictionary* inputType = configuration[@"inputType"];
NSString* keyboardAppearance = configuration[@"keyboardAppearance"];
if ([configuration[@"obscureText"] boolValue]) {
_activeView = _secureView;
NSArray* fields = configuration[@"fields"];
NSString* clientUniqueId = _uniqueIdFromDictionary(configuration);
bool isSecureTextEntry = [configuration[@"obscureText"] boolValue];

if (fields == nil) {
_activeView = isSecureTextEntry ? _nonAutofillSecureInputView : _nonAutofillInputView;
[FlutterTextInputPlugin setupInputView:_activeView WithConfiguration:configuration];

if (![_activeView isDescendantOfView:_inputHider]) {
[_inputHider addSubview:_activeView];
}
} else {
_activeView = _view;
NSAssert(clientUniqueId != nil, @"The client's unique id can't be null");
for (FlutterTextInputView* v in _inputViews)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

for (int i = 0; i < 10; i++)
  BlowTheHorn();                // AVOID.

http://google.github.io/styleguide/objcguide.html#conditionals

[v removeFromSuperview];
for (UIView* subview in [_inputHider subviews])
[subview removeFromSuperview];

[_inputViews removeAllObjects];

for (NSDictionary* field in fields) {
FlutterTextInputView* newInputView = [[FlutterTextInputView alloc] init];
newInputView.textInputDelegate = _textInputDelegate;
[_inputViews addObject:newInputView];
Comment thread
gaaclarke marked this conversation as resolved.

NSString* autofillId = _uniqueIdFromDictionary(field);
[newInputView setAutofillId:autofillId];

if ([clientUniqueId isEqualToString:autofillId])
_activeView = newInputView;

[FlutterTextInputPlugin setupInputView:newInputView WithConfiguration:field];
[_inputHider addSubview:newInputView];
}
}

_activeView.keyboardType = ToUIKeyboardType(inputType);
_activeView.returnKeyType = ToUIReturnKeyType(configuration[@"inputAction"]);
_activeView.autocapitalizationType = ToUITextAutoCapitalizationType(configuration);
[_activeView setTextInputClient:client];
[_activeView reloadInputViews];
}

+ (void)setupInputView:(FlutterTextInputView*)inputView
WithConfiguration:(NSDictionary*)configuration {
NSDictionary* inputType = configuration[@"inputType"];
NSString* keyboardAppearance = configuration[@"keyboardAppearance"];
NSDictionary* autofill = configuration[@"autofill"];

inputView.secureTextEntry = [configuration[@"obscureText"] boolValue];
inputView.keyboardType = ToUIKeyboardType(inputType);
inputView.returnKeyType = ToUIReturnKeyType(configuration[@"inputAction"]);
inputView.autocapitalizationType = ToUITextAutoCapitalizationType(configuration);

if (@available(iOS 11.0, *)) {
NSString* smartDashesType = configuration[@"smartDashesType"];
// This index comes from the SmartDashesType enum in the framework.
bool smartDashesIsDisabled = smartDashesType && [smartDashesType isEqualToString:@"0"];
_activeView.smartDashesType =
inputView.smartDashesType =
smartDashesIsDisabled ? UITextSmartDashesTypeNo : UITextSmartDashesTypeYes;
NSString* smartQuotesType = configuration[@"smartQuotesType"];
// This index comes from the SmartQuotesType enum in the framework.
bool smartQuotesIsDisabled = smartQuotesType && [smartQuotesType isEqualToString:@"0"];
_activeView.smartQuotesType =
inputView.smartQuotesType =
smartQuotesIsDisabled ? UITextSmartQuotesTypeNo : UITextSmartQuotesTypeYes;
}
if ([keyboardAppearance isEqualToString:@"Brightness.dark"]) {
_activeView.keyboardAppearance = UIKeyboardAppearanceDark;
inputView.keyboardAppearance = UIKeyboardAppearanceDark;
} else if ([keyboardAppearance isEqualToString:@"Brightness.light"]) {
_activeView.keyboardAppearance = UIKeyboardAppearanceLight;
inputView.keyboardAppearance = UIKeyboardAppearanceLight;
} else {
_activeView.keyboardAppearance = UIKeyboardAppearanceDefault;
inputView.keyboardAppearance = UIKeyboardAppearanceDefault;
}
NSString* autocorrect = configuration[@"autocorrect"];
_activeView.autocorrectionType = autocorrect && ![autocorrect boolValue]
? UITextAutocorrectionTypeNo
: UITextAutocorrectionTypeDefault;
[_activeView setTextInputClient:client];
[_activeView reloadInputViews];
inputView.autocorrectionType = autocorrect && ![autocorrect boolValue]
? UITextAutocorrectionTypeNo
: UITextAutocorrectionTypeDefault;
if (@available(iOS 10.0, *)) {
if (autofill == nil) {
inputView.textContentType = @"";
} else {
inputView.textContentType = ToUITextContentType(autofill[@"hints"]);
[inputView setTextInputState:autofill[@"editingValue"]];
// An input field needs to be visible in order to get
// autofilled when it's not the one that triggered
// autofill.
inputView.frame = CGRectMake(0, 0, 1, 1);
}
}
}

- (void)setTextInputEditingState:(NSDictionary*)state {
Expand Down
Loading