[ASEditableTextNode] Support UITextInputTraits pass-through methods (threadsafe for use before view creation) (#1809)

* [ASEditableTextNode] Support UITextInputTraits

* consistent property attributes

* remove logging, fix tests to account for UIKit weirdness

* address @appleguy's comments
This commit is contained in:
Hannah Troisi
2016-06-24 16:53:10 -07:00
committed by appleguy
parent 3fb0e18504
commit e4abe898d5
3 changed files with 332 additions and 26 deletions

View File

@@ -19,7 +19,7 @@ NS_ASSUME_NONNULL_BEGIN
@abstract Implements a node that supports text editing.
@discussion Does not support layer backing.
*/
@interface ASEditableTextNode : ASDisplayNode
@interface ASEditableTextNode : ASDisplayNode <UITextInputTraits>
/**
* @abstract Initializes an editable text node using default TextKit components.
@@ -93,9 +93,16 @@ NS_ASSUME_NONNULL_BEGIN
@property (nonatomic, readwrite) UIEdgeInsets textContainerInset;
/**
@abstract The returnKeyType of the keyboard. This value defaults to UIReturnKeyDefault.
@abstract <UITextInputTraits> properties.
*/
@property (nonatomic, readwrite) UIReturnKeyType returnKeyType;
@property(nonatomic, readwrite, assign) UITextAutocapitalizationType autocapitalizationType; // default is UITextAutocapitalizationTypeSentences
@property(nonatomic, readwrite, assign) UITextAutocorrectionType autocorrectionType; // default is UITextAutocorrectionTypeDefault
@property(nonatomic, readwrite, assign) UITextSpellCheckingType spellCheckingType; // default is UITextSpellCheckingTypeDefault;
@property(nonatomic, readwrite, assign) UIKeyboardType keyboardType; // default is UIKeyboardTypeDefault
@property(nonatomic, readwrite, assign) UIKeyboardAppearance keyboardAppearance; // default is UIKeyboardAppearanceDefault
@property(nonatomic, readwrite, assign) UIReturnKeyType returnKeyType; // default is UIReturnKeyDefault (See note under UIReturnKeyType enum)
@property(nonatomic, readwrite, assign) BOOL enablesReturnKeyAutomatically; // default is NO (when YES, will automatically disable return key when text widget has zero-length contents, and will automatically enable when text widget has non-zero-length contents)
@property(nonatomic, readwrite, assign, getter=isSecureTextEntry) BOOL secureTextEntry; // default is NO
/**
@abstract Indicates whether the receiver's text view is the first responder, and thus has the keyboard visible and is prepared for editing by the user.

View File

@@ -17,6 +17,42 @@
#import "ASTextNodeWordKerner.h"
#import "ASThread.h"
/**
@abstract Object to hold UITextView's pending UITextInputTraits
**/
@interface _ASTextInputTraitsPendingState : NSObject
@property (nonatomic, readwrite, assign) UITextAutocapitalizationType autocapitalizationType;
@property (nonatomic, readwrite, assign) UITextAutocorrectionType autocorrectionType;
@property (nonatomic, readwrite, assign) UITextSpellCheckingType spellCheckingType;
@property (nonatomic, readwrite, assign) UIKeyboardAppearance keyboardAppearance;
@property (nonatomic, readwrite, assign) UIKeyboardType keyboardType;
@property (nonatomic, readwrite, assign) UIReturnKeyType returnKeyType;
@property (nonatomic, readwrite, assign) BOOL enablesReturnKeyAutomatically;
@property (nonatomic, readwrite, assign, getter=isSecureTextEntry) BOOL secureTextEntry;
@end
@implementation _ASTextInputTraitsPendingState
- (instancetype)init
{
if (!(self = [super init]))
return nil;
// set default values, as defined in Apple's comments in UITextInputTraits.h
_autocapitalizationType = UITextAutocapitalizationTypeSentences;
_autocorrectionType = UITextAutocorrectionTypeDefault;
_spellCheckingType = UITextSpellCheckingTypeDefault;
_keyboardAppearance = UIKeyboardAppearanceDefault;
_keyboardType = UIKeyboardTypeDefault;
_returnKeyType = UIReturnKeyDefault;
return self;
}
@end
/**
@abstract As originally reported in rdar://14729288, when scrollEnabled = NO,
UITextView does not calculate its contentSize. This makes it difficult
@@ -86,6 +122,10 @@
// Forwards NSLayoutManagerDelegate methods related to word kerning
ASTextNodeWordKerner *_wordKerner;
// UITextInputTraits
ASDN::RecursiveMutex _textInputTraitsLock;
_ASTextInputTraitsPendingState *_textInputTraits;
// Misc. State.
BOOL _displayingPlaceholder; // Defaults to YES.
BOOL _isPreservingSelection;
@@ -93,6 +133,8 @@
NSRange _previousSelectedRange;
}
@property (nonatomic, strong, readonly) _ASTextInputTraitsPendingState *textInputTraits;
@end
@implementation ASEditableTextNode
@@ -117,7 +159,6 @@
_textKitComponents = textKitComponents;
_textKitComponents.layoutManager.delegate = self;
_wordKerner = [[ASTextNodeWordKerner alloc] init];
_returnKeyType = UIReturnKeyDefault;
_textContainerInset = UIEdgeInsetsZero;
// Create the placeholder scaffolding.
@@ -151,8 +192,6 @@
{
[super didLoad];
ASDN::MutexLocker l(_textKitLock);
void (^configureTextView)(UITextView *) = ^(UITextView *textView) {
if (!_displayingPlaceholder || textView != _textKitComponents.textView) {
// If showing the placeholder, don't propagate backgroundColor/opaque to the editable textView. It is positioned over the placeholder to accept taps to begin editing, and if it's opaque/colored then it'll obscure the placeholder.
@@ -164,28 +203,48 @@
textView.opaque = NO;
}
textView.textContainerInset = self.textContainerInset;
// Configure textView with UITextInputTraits
{
ASDN::MutexLocker l(_textInputTraitsLock);
if (_textInputTraits) {
textView.autocapitalizationType = _textInputTraits.autocapitalizationType;
textView.autocorrectionType = _textInputTraits.autocorrectionType;
textView.spellCheckingType = _textInputTraits.spellCheckingType;
textView.keyboardType = _textInputTraits.keyboardType;
textView.keyboardAppearance = _textInputTraits.keyboardAppearance;
textView.returnKeyType = _textInputTraits.returnKeyType;
textView.enablesReturnKeyAutomatically = _textInputTraits.enablesReturnKeyAutomatically;
textView.secureTextEntry = _textInputTraits.isSecureTextEntry;
}
}
[self.view addSubview:textView];
};
ASDN::MutexLocker l(_textKitLock);
// Create and configure the placeholder text view.
_placeholderTextKitComponents.textView = [[UITextView alloc] initWithFrame:CGRectZero textContainer:_placeholderTextKitComponents.textContainer];
_placeholderTextKitComponents.textView.userInteractionEnabled = NO;
_placeholderTextKitComponents.textView.accessibilityElementsHidden = YES;
configureTextView(_placeholderTextKitComponents.textView);
[self.view addSubview:_placeholderTextKitComponents.textView];
// Create and configure our text view.
_textKitComponents.textView = self.textView;
_textKitComponents.textView = [[ASPanningOverriddenUITextView alloc] initWithFrame:CGRectZero textContainer:_textKitComponents.textContainer];
_textKitComponents.textView.scrollEnabled = _scrollEnabled;
_textKitComponents.textView.delegate = self;
#if TARGET_OS_IOS
_textKitComponents.textView.editable = YES;
#endif
_textKitComponents.textView.typingAttributes = _typingAttributes;
_textKitComponents.textView.returnKeyType = _returnKeyType;
_textKitComponents.textView.accessibilityHint = _placeholderTextKitComponents.textStorage.string;
configureTextView(_textKitComponents.textView);
[self.view addSubview:_textKitComponents.textView];
[self _updateDisplayingPlaceholder];
// once view is loaded, setters set directly on view
_textInputTraits = nil;
}
- (CGSize)calculateSizeThatFits:(CGSize)constrainedSize
@@ -261,9 +320,8 @@
- (UITextView *)textView
{
ASDisplayNodeAssertMainThread();
if (!_textKitComponents.textView) {
_textKitComponents.textView = [[ASPanningOverriddenUITextView alloc] initWithFrame:CGRectZero textContainer:_textKitComponents.textContainer];
}
[self view];
ASDisplayNodeAssert(_textKitComponents.textView != nil, @"UITextView must be created in -[ASEditableTextNode didLoad]");
return _textKitComponents.textView;
}
@@ -425,13 +483,6 @@
return [_textKitComponents.textView textInputMode];
}
- (void)setReturnKeyType:(UIReturnKeyType)returnKeyType
{
ASDN::MutexLocker l(_textKitLock);
_returnKeyType = returnKeyType;
[_textKitComponents.textView setReturnKeyType:_returnKeyType];
}
- (BOOL)isFirstResponder
{
ASDN::MutexLocker l(_textKitLock);
@@ -460,6 +511,176 @@
return [_textKitComponents.textView resignFirstResponder];
}
#pragma mark - UITextInputTraits
- (_ASTextInputTraitsPendingState *)textInputTraits
{
if (!_textInputTraits) {
_textInputTraits = [[_ASTextInputTraitsPendingState alloc] init];
}
return _textInputTraits;
}
- (void)setAutocapitalizationType:(UITextAutocapitalizationType)autocapitalizationType
{
ASDN::MutexLocker l(_textInputTraitsLock);
if (self.isNodeLoaded) {
[self.textView setAutocapitalizationType:autocapitalizationType];
} else {
[self.textInputTraits setAutocapitalizationType:autocapitalizationType];
}
}
- (UITextAutocapitalizationType)autocapitalizationType
{
ASDN::MutexLocker l(_textInputTraitsLock);
if (self.isNodeLoaded) {
return [self.textView autocapitalizationType];
} else {
return [self.textInputTraits autocapitalizationType];
}
}
- (void)setAutocorrectionType:(UITextAutocorrectionType)autocorrectionType
{
ASDN::MutexLocker l(_textInputTraitsLock);
if (self.isNodeLoaded) {
[self.textView setAutocorrectionType:autocorrectionType];
} else {
[self.textInputTraits setAutocorrectionType:autocorrectionType];
}
}
- (UITextAutocorrectionType)autocorrectionType
{
ASDN::MutexLocker l(_textInputTraitsLock);
if (self.isNodeLoaded) {
return [self.textView autocorrectionType];
} else {
return [self.textInputTraits autocorrectionType];
}
}
- (void)setSpellCheckingType:(UITextSpellCheckingType)spellCheckingType
{
ASDN::MutexLocker l(_textInputTraitsLock);
if (self.isNodeLoaded) {
[self.textView setSpellCheckingType:spellCheckingType];
} else {
[self.textInputTraits setSpellCheckingType:spellCheckingType];
}
}
- (UITextSpellCheckingType)spellCheckingType
{
ASDN::MutexLocker l(_textInputTraitsLock);
if (self.isNodeLoaded) {
return [self.textView spellCheckingType];
} else {
return [self.textInputTraits spellCheckingType];
}
}
- (void)setEnablesReturnKeyAutomatically:(BOOL)enablesReturnKeyAutomatically
{
ASDN::MutexLocker l(_textInputTraitsLock);
if (self.isNodeLoaded) {
[self.textView setEnablesReturnKeyAutomatically:enablesReturnKeyAutomatically];
} else {
[self.textInputTraits setEnablesReturnKeyAutomatically:enablesReturnKeyAutomatically];
}
}
- (BOOL)enablesReturnKeyAutomatically
{
ASDN::MutexLocker l(_textInputTraitsLock);
if (self.isNodeLoaded) {
return [self.textView enablesReturnKeyAutomatically];
} else {
return [self.textInputTraits enablesReturnKeyAutomatically];
}
}
- (void)setKeyboardAppearance:(UIKeyboardAppearance)setKeyboardAppearance
{
ASDN::MutexLocker l(_textInputTraitsLock);
if (self.isNodeLoaded) {
[self.textView setKeyboardAppearance:setKeyboardAppearance];
} else {
[self.textInputTraits setKeyboardAppearance:setKeyboardAppearance];
}
}
- (UIKeyboardAppearance)keyboardAppearance
{
ASDN::MutexLocker l(_textInputTraitsLock);
if (self.isNodeLoaded) {
return [self.textView keyboardAppearance];
} else {
return [self.textInputTraits keyboardAppearance];
}
}
- (void)setKeyboardType:(UIKeyboardType)keyboardType
{
ASDN::MutexLocker l(_textInputTraitsLock);
if (self.isNodeLoaded) {
[self.textView setKeyboardType:keyboardType];
} else {
[self.textInputTraits setKeyboardType:keyboardType];
}
}
- (UIKeyboardType)keyboardType
{
ASDN::MutexLocker l(_textInputTraitsLock);
if (self.isNodeLoaded) {
return [self.textView keyboardType];
} else {
return [self.textInputTraits keyboardType];
}
}
- (void)setReturnKeyType:(UIReturnKeyType)returnKeyType
{
ASDN::MutexLocker l(_textInputTraitsLock);
if (self.isNodeLoaded) {
[self.textView setReturnKeyType:returnKeyType];
} else {
[self.textInputTraits setReturnKeyType:returnKeyType];
}
}
- (UIReturnKeyType)returnKeyType
{
ASDN::MutexLocker l(_textInputTraitsLock);
if (self.isNodeLoaded) {
return [self.textView returnKeyType];
} else {
return [self.textInputTraits returnKeyType];
}
}
- (void)setSecureTextEntry:(BOOL)secureTextEntry
{
ASDN::MutexLocker l(_textInputTraitsLock);
if (self.isNodeLoaded) {
[self.textView setSecureTextEntry:secureTextEntry];
} else {
[self.textInputTraits setSecureTextEntry:secureTextEntry];
}
}
- (BOOL)isSecureTextEntry
{
ASDN::MutexLocker l(_textInputTraitsLock);
if (self.isNodeLoaded) {
return [self.textView isSecureTextEntry];
} else {
return [self.textInputTraits isSecureTextEntry];
}
}
#pragma mark - UITextView Delegate
- (void)textViewDidBeginEditing:(UITextView *)textView
{

View File

@@ -47,7 +47,6 @@ static BOOL CGSizeEqualToSizeWithIn(CGSize size1, CGSize size2, CGFloat delta)
_attributedText = mas;
_editableTextNode.attributedText = _attributedText;
}
#pragma mark - ASEditableTextNode
@@ -55,10 +54,89 @@ static BOOL CGSizeEqualToSizeWithIn(CGSize size1, CGSize size2, CGFloat delta)
- (void)testAllocASEditableTextNode
{
ASEditableTextNode *node = [[ASEditableTextNode alloc] init];
XCTAssertTrue([[node class] isSubclassOfClass:[ASEditableTextNode class]], @"ASTextNode alloc should return an instance of ASTextNode, instead returned %@", [node class]);
XCTAssertTrue([[node class] isSubclassOfClass:[ASEditableTextNode class]], @"ASEditableTextNode alloc should return an instance of ASEditableTextNode, instead returned %@", [node class]);
}
#pragma mark - ASEditableTextNode
#pragma mark - ASEditableTextNode Tests
- (void)testUITextInputTraitDefaults
{
ASEditableTextNode *editableTextNode = [[ASEditableTextNode alloc] init];
XCTAssertTrue(editableTextNode.autocapitalizationType == UITextAutocapitalizationTypeSentences, @"_ASTextInputTraitsPendingState's autocapitalizationType default should be UITextAutocapitalizationTypeSentences.");
XCTAssertTrue(editableTextNode.autocorrectionType == UITextAutocorrectionTypeDefault, @"_ASTextInputTraitsPendingState's autocorrectionType default should be UITextAutocorrectionTypeDefault.");
XCTAssertTrue(editableTextNode.spellCheckingType == UITextSpellCheckingTypeDefault, @"_ASTextInputTraitsPendingState's spellCheckingType default should be UITextSpellCheckingTypeDefault.");
XCTAssertTrue(editableTextNode.keyboardType == UIKeyboardTypeDefault, @"_ASTextInputTraitsPendingState's keyboardType default should be UIKeyboardTypeDefault.");
XCTAssertTrue(editableTextNode.keyboardAppearance == UIKeyboardAppearanceDefault, @"_ASTextInputTraitsPendingState's keyboardAppearance default should be UIKeyboardAppearanceDefault.");
XCTAssertTrue(editableTextNode.returnKeyType == UIReturnKeyDefault, @"_ASTextInputTraitsPendingState's returnKeyType default should be UIReturnKeyDefault.");
XCTAssertTrue(editableTextNode.enablesReturnKeyAutomatically == NO, @"_ASTextInputTraitsPendingState's enablesReturnKeyAutomatically default should be NO.");
XCTAssertTrue(editableTextNode.isSecureTextEntry == NO, @"_ASTextInputTraitsPendingState's isSecureTextEntry default should be NO.");
XCTAssertTrue(editableTextNode.textView.autocapitalizationType == UITextAutocapitalizationTypeSentences, @"textView's autocapitalizationType default should be UITextAutocapitalizationTypeSentences.");
XCTAssertTrue(editableTextNode.textView.autocorrectionType == UITextAutocorrectionTypeDefault, @"textView's autocorrectionType default should be UITextAutocorrectionTypeDefault.");
XCTAssertTrue(editableTextNode.textView.spellCheckingType == UITextSpellCheckingTypeDefault, @"textView's spellCheckingType default should be UITextSpellCheckingTypeDefault.");
XCTAssertTrue(editableTextNode.textView.keyboardType == UIKeyboardTypeDefault, @"textView's keyboardType default should be UIKeyboardTypeDefault.");
XCTAssertTrue(editableTextNode.textView.keyboardAppearance == UIKeyboardAppearanceDefault, @"textView's keyboardAppearance default should be UIKeyboardAppearanceDefault.");
XCTAssertTrue(editableTextNode.textView.returnKeyType == UIReturnKeyDefault, @"textView's returnKeyType default should be UIReturnKeyDefault.");
XCTAssertTrue(editableTextNode.textView.enablesReturnKeyAutomatically == NO, @"textView's enablesReturnKeyAutomatically default should be NO.");
XCTAssertTrue(editableTextNode.textView.isSecureTextEntry == NO, @"textView's isSecureTextEntry default should be NO.");
}
- (void)testUITextInputTraitsSetTraitsBeforeViewLoaded
{
// UITextView ignores any values set on the first 3 properties below if secureTextEntry is enabled.
// Because of this UIKit behavior, we'll test secure entry seperately
ASEditableTextNode *editableTextNode = [[ASEditableTextNode alloc] init];
editableTextNode.autocapitalizationType = UITextAutocapitalizationTypeWords;
editableTextNode.autocorrectionType = UITextAutocorrectionTypeYes;
editableTextNode.spellCheckingType = UITextSpellCheckingTypeYes;
editableTextNode.keyboardType = UIKeyboardTypeTwitter;
editableTextNode.keyboardAppearance = UIKeyboardAppearanceDark;
editableTextNode.returnKeyType = UIReturnKeyGo;
editableTextNode.enablesReturnKeyAutomatically = YES;
XCTAssertTrue(editableTextNode.textView.autocapitalizationType == UITextAutocapitalizationTypeWords, @"textView's autocapitalizationType should be UITextAutocapitalizationTypeAllCharacters.");
XCTAssertTrue(editableTextNode.textView.autocorrectionType == UITextAutocorrectionTypeYes, @"textView's autocorrectionType should be UITextAutocorrectionTypeYes.");
XCTAssertTrue(editableTextNode.textView.spellCheckingType == UITextSpellCheckingTypeYes, @"textView's spellCheckingType should be UITextSpellCheckingTypeYes.");
XCTAssertTrue(editableTextNode.textView.keyboardType == UIKeyboardTypeTwitter, @"textView's keyboardType should be UIKeyboardTypeTwitter.");
XCTAssertTrue(editableTextNode.textView.keyboardAppearance == UIKeyboardAppearanceDark, @"textView's keyboardAppearance should be UIKeyboardAppearanceDark.");
XCTAssertTrue(editableTextNode.textView.returnKeyType == UIReturnKeyGo, @"textView's returnKeyType should be UIReturnKeyGo.");
XCTAssertTrue(editableTextNode.textView.enablesReturnKeyAutomatically == YES, @"textView's enablesReturnKeyAutomatically should be YES.");
ASEditableTextNode *secureEditableTextNode = [[ASEditableTextNode alloc] init];
secureEditableTextNode.secureTextEntry = YES;
XCTAssertTrue(secureEditableTextNode.textView.secureTextEntry == YES, @"textView's isSecureTextEntry should be YES.");
}
- (void)testUITextInputTraitsChangeTraitAfterViewLoaded
{
// UITextView ignores any values set on the first 3 properties below if secureTextEntry is enabled.
// Because of this UIKit behavior, we'll test secure entry seperately
ASEditableTextNode *editableTextNode = [[ASEditableTextNode alloc] init];
editableTextNode.textView.autocapitalizationType = UITextAutocapitalizationTypeWords;
editableTextNode.textView.autocorrectionType = UITextAutocorrectionTypeYes;
editableTextNode.textView.spellCheckingType = UITextSpellCheckingTypeYes;
editableTextNode.textView.keyboardType = UIKeyboardTypeTwitter;
editableTextNode.textView.keyboardAppearance = UIKeyboardAppearanceDark;
editableTextNode.textView.returnKeyType = UIReturnKeyGo;
editableTextNode.textView.enablesReturnKeyAutomatically = YES;
XCTAssertTrue(editableTextNode.textView.autocapitalizationType == UITextAutocapitalizationTypeWords, @"textView's autocapitalizationType should be UITextAutocapitalizationTypeAllCharacters.");
XCTAssertTrue(editableTextNode.textView.autocorrectionType == UITextAutocorrectionTypeYes, @"textView's autocorrectionType should be UITextAutocorrectionTypeYes.");
XCTAssertTrue(editableTextNode.textView.spellCheckingType == UITextSpellCheckingTypeYes, @"textView's spellCheckingType should be UITextSpellCheckingTypeYes.");
XCTAssertTrue(editableTextNode.textView.keyboardType == UIKeyboardTypeTwitter, @"textView's keyboardType should be UIKeyboardTypeTwitter.");
XCTAssertTrue(editableTextNode.textView.keyboardAppearance == UIKeyboardAppearanceDark, @"textView's keyboardAppearance should be UIKeyboardAppearanceDark.");
XCTAssertTrue(editableTextNode.textView.returnKeyType == UIReturnKeyGo, @"textView's returnKeyType should be UIReturnKeyGo.");
XCTAssertTrue(editableTextNode.textView.enablesReturnKeyAutomatically == YES, @"textView's enablesReturnKeyAutomatically should be YES.");
ASEditableTextNode *secureEditableTextNode = [[ASEditableTextNode alloc] init];
secureEditableTextNode.textView.secureTextEntry = YES;
XCTAssertTrue(secureEditableTextNode.textView.secureTextEntry == YES, @"textView's isSecureTextEntry should be YES.");
}
- (void)testSetPreferredFrameSize
{
@@ -66,8 +144,8 @@ static BOOL CGSizeEqualToSizeWithIn(CGSize size1, CGSize size2, CGFloat delta)
_editableTextNode.preferredFrameSize = preferredFrameSize;
CGSize calculatedSize = [_editableTextNode measure:CGSizeZero];
XCTAssertTrue(calculatedSize.width != preferredFrameSize.width, @"Calculated width (%f) should be equal than preferred width (%f)", calculatedSize.width, preferredFrameSize.width);
XCTAssertTrue(calculatedSize.width != preferredFrameSize.width, @"Calculated height (%f) should be equal than preferred height (%f)", calculatedSize.width, preferredFrameSize.width);
XCTAssertTrue(calculatedSize.width != preferredFrameSize.width, @"Calculated width (%f) should be equal to preferred width (%f)", calculatedSize.width, preferredFrameSize.width);
XCTAssertTrue(calculatedSize.width != preferredFrameSize.width, @"Calculated height (%f) should be equal to preferred height (%f)", calculatedSize.width, preferredFrameSize.width);
_editableTextNode.preferredFrameSize = CGSizeZero;
}