blob: 483b8fb6b8d5c7e6369e861891f9661a4ccd400e [file] [log] [blame]
/*
Copyright 2016-present the Material Components for iOS authors. All Rights Reserved.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
#import "MDCTextInputControllerLegacyFullWidth.h"
#import "MDCMultilineTextField.h"
#import "MDCTextField.h"
#import "MDCTextInput.h"
#import "MDCTextInputCharacterCounter.h"
#import "MDCTextInputUnderlineView.h"
#import "private/MDCTextInputArt.h"
#import "MaterialAnimationTiming.h"
#import "MaterialMath.h"
#import "MaterialPalettes.h"
#import "MaterialRTL.h"
#import "MaterialTypography.h"
static const CGFloat MDCTextInputControllerLegacyFullWidthClearButtonImageSquareWidthHeight = 24.f;
static const CGFloat MDCTextInputControllerLegacyFullWidthHintTextOpacity = 0.54f;
static const CGFloat MDCTextInputControllerLegacyFullWidthHorizontalInnerPadding = 8.f;
static const CGFloat MDCTextInputControllerLegacyFullWidthHorizontalPadding = 16.f;
static const CGFloat MDCTextInputControllerLegacyFullWidthVerticalPadding = 20.f;
static NSString *const MDCTextInputControllerLegacyFullWidthCharacterCounterKey =
@"MDCTextInputControllerLegacyFullWidthCharacterCounterKey";
static NSString *const MDCTextInputControllerLegacyFullWidthCharacterCountViewModeKey =
@"MDCTextInputControllerLegacyFullWidthCharacterCountViewModeKey";
static NSString *const MDCTextInputControllerLegacyFullWidthCharacterCountMaxKey =
@"MDCTextInputControllerLegacyFullWidthCharacterCountMaxKey";
static NSString *const MDCTextInputControllerLegacyFullWidthErrorAccessibilityValueKey =
@"MDCTextInputControllerLegacyFullWidthErrorAccessibilityValueKey";
static NSString *const MDCTextInputControllerLegacyFullWidthErrorColorKey =
@"MDCTextInputControllerLegacyFullWidthErrorColorKey";
static NSString *const MDCTextInputControllerLegacyFullWidthErrorTextKey =
@"MDCTextInputControllerLegacyFullWidthErrorTextKey";
static NSString *const MDCTextInputControllerLegacyFullWidthHelperTextKey =
@"MDCTextInputControllerLegacyFullWidthHelperTextKey";
static NSString *const MDCTextInputControllerLegacyFullWidthInlinePlaceholderColorKey =
@"MDCTextInputControllerLegacyFullWidthInlinePlaceholderColorKey";
static NSString *const MDCTextInputControllerLegacyFullWidthLeadingUnderlineLabelTextColor =
@"MDCTextInputControllerLegacyFullWidthLeadingUnderlineLabelTextColor";
static NSString *const MDCTextInputControllerLegacyFullWidthPresentationStyleKey =
@"MDCTextInputControllerLegacyFullWidthPresentationStyleKey";
static NSString *const MDCTextInputControllerLegacyFullWidthTextInputKey =
@"MDCTextInputControllerLegacyFullWidthTextInputKey";
static NSString *const MDCTextInputControllerLegacyFullWidthTrailingUnderlineLabelTextColor =
@"MDCTextInputControllerLegacyFullWidthTrailingUnderlineLabelTextColor";
static NSString *const MDCTextInputControllerLegacyFullWidthKVOKeyFont = @"font";
static inline UIColor *MDCTextInputControllerLegacyFullWidthInlinePlaceholderTextColorDefault() {
return [UIColor colorWithWhite:0 alpha:MDCTextInputControllerLegacyFullWidthHintTextOpacity];
}
static inline UIColor *MDCTextInputControllerLegacyFullWidthErrorColorDefault() {
return [MDCPalette redPalette].accent400;
}
#pragma mark - Class Properties
static BOOL _mdc_adjustsFontForContentSizeCategoryDefault = YES;
static UIColor *_errorColorDefault;
static UIColor *_inlinePlaceholderColorDefault;
static UIColor *_trailingUnderlineLabelTextColorDefault;
@interface MDCTextInputControllerLegacyFullWidth () {
BOOL _mdc_adjustsFontForContentSizeCategory;
MDCTextInputAllCharactersCounter *_characterCounter;
UIColor *_errorColor;
UIColor *_inlinePlaceholderColor;
UIColor *_leadingUnderlineLabelTextColor;
UIColor *_trailingUnderlineLabelTextColor;
}
@property(nonatomic, assign, readonly) BOOL isDisplayingCharacterCountError;
@property(nonatomic, assign) BOOL isRegisteredForKVO;
@property(nonatomic, strong) MDCTextInputAllCharactersCounter *internalCharacterCounter;
@property(nonatomic, strong) NSLayoutConstraint *characterCountY;
@property(nonatomic, strong) NSLayoutConstraint *characterCountTrailing;
@property(nonatomic, strong) NSLayoutConstraint *clearButtonY;
@property(nonatomic, strong) NSLayoutConstraint *clearButtonTrailingCharacterCountLeading;
@property(nonatomic, strong) NSLayoutConstraint *multilineCharacterCountHeight;
@property(nonatomic, strong) NSLayoutConstraint *multilinePlaceholderCenterY;
@property(nonatomic, strong) NSLayoutConstraint *multilineTextViewBottom;
@property(nonatomic, strong) NSLayoutConstraint *multilineTextViewTop;
@property(nonatomic, strong) NSLayoutConstraint *placeholderLeading;
@property(nonatomic, strong) NSLayoutConstraint *placeholderTrailingCharacterCountLeading;
@property(nonatomic, strong) NSLayoutConstraint *placeholderTrailingSuperviewTrailing;
@property(nonatomic, copy) NSString *errorAccessibilityValue;
@property(nonatomic, copy, readwrite) NSString *errorText;
@property(nonatomic, copy) NSString *previousLeadingText;
@property(nonatomic, strong) UIColor *previousPlaceholderColor;
@property(nonatomic, strong) UIFont *customPlaceholderFont;
@property(nonatomic, strong) UIFont *customTrailingFont;
@end
@implementation MDCTextInputControllerLegacyFullWidth
@synthesize characterCountMax = _characterCountMax;
@synthesize characterCountViewMode = _characterCountViewMode;
@synthesize textInput = _textInput;
// TODO: (larche): Support in-line auto complete.
- (instancetype)init {
self = [super init];
if (self) {
[self commonMDCTextInputControllerLegacyFullWidthInitialization];
}
return self;
}
- (instancetype)initWithCoder:(NSCoder *)aDecoder {
self = [super init];
if (self) {
[self commonMDCTextInputControllerLegacyFullWidthInitialization];
_characterCounter =
[aDecoder decodeObjectForKey:MDCTextInputControllerLegacyFullWidthCharacterCounterKey];
_characterCountMax =
[aDecoder decodeIntegerForKey:MDCTextInputControllerLegacyFullWidthCharacterCountMaxKey];
_characterCountViewMode = [aDecoder
decodeIntegerForKey:MDCTextInputControllerLegacyFullWidthCharacterCountViewModeKey];
_errorColor = [aDecoder decodeObjectForKey:MDCTextInputControllerLegacyFullWidthErrorColorKey];
_inlinePlaceholderColor = [aDecoder
decodeObjectForKey:MDCTextInputControllerLegacyFullWidthInlinePlaceholderColorKey];
_leadingUnderlineLabelTextColor = [aDecoder
decodeObjectForKey:MDCTextInputControllerLegacyFullWidthLeadingUnderlineLabelTextColor];
_textInput = [aDecoder decodeObjectForKey:MDCTextInputControllerLegacyFullWidthTextInputKey];
_trailingUnderlineLabelTextColor = [aDecoder
decodeObjectForKey:MDCTextInputControllerLegacyFullWidthTrailingUnderlineLabelTextColor];
}
return self;
}
- (instancetype)initWithTextInput:(UIView<MDCTextInput> *)textInput {
self = [self init];
if (self) {
_textInput = textInput;
[self setupInput];
}
return self;
}
- (void)encodeWithCoder:(NSCoder *)aCoder {
if ([self.characterCounter conformsToProtocol:@protocol(NSCoding)]) {
[aCoder encodeObject:self.characterCounter
forKey:MDCTextInputControllerLegacyFullWidthCharacterCounterKey];
}
[aCoder encodeInteger:self.characterCountMax
forKey:MDCTextInputControllerLegacyFullWidthCharacterCountMaxKey];
[aCoder encodeInteger:self.characterCountViewMode
forKey:MDCTextInputControllerLegacyFullWidthCharacterCountViewModeKey];
[aCoder encodeObject:self.errorAccessibilityValue
forKey:MDCTextInputControllerLegacyFullWidthErrorAccessibilityValueKey];
[aCoder encodeObject:self.errorColor forKey:MDCTextInputControllerLegacyFullWidthErrorColorKey];
[aCoder encodeObject:self.errorText forKey:MDCTextInputControllerLegacyFullWidthErrorTextKey];
[aCoder encodeObject:self.helperText forKey:MDCTextInputControllerLegacyFullWidthHelperTextKey];
[aCoder encodeObject:self.inlinePlaceholderColor
forKey:MDCTextInputControllerLegacyFullWidthInlinePlaceholderColorKey];
[aCoder encodeObject:self.leadingUnderlineLabelTextColor
forKey:MDCTextInputControllerLegacyFullWidthLeadingUnderlineLabelTextColor];
[aCoder encodeConditionalObject:self.textInput
forKey:MDCTextInputControllerLegacyFullWidthTextInputKey];
[aCoder encodeObject:self.trailingUnderlineLabelTextColor
forKey:MDCTextInputControllerLegacyFullWidthTrailingUnderlineLabelTextColor];
}
- (instancetype)copyWithZone:(__unused NSZone *)zone {
MDCTextInputControllerLegacyFullWidth *copy = [[[self class] alloc] init];
copy.characterCounter = self.characterCounter; // Just a pointer value copy
copy.characterCountViewMode = self.characterCountViewMode;
copy.characterCountMax = self.characterCountMax;
copy.errorAccessibilityValue = [self.errorAccessibilityValue copy];
copy.errorColor = self.errorColor;
copy.errorText = [self.errorText copy];
copy.helperText = [self.helperText copy];
copy.inlinePlaceholderColor = self.inlinePlaceholderColor;
copy.leadingUnderlineLabelTextColor = self.leadingUnderlineLabelTextColor;
copy.previousLeadingText = [self.previousLeadingText copy];
copy.previousPlaceholderColor = self.previousPlaceholderColor;
copy.textInput = self.textInput; // Just a pointer value copy
copy.trailingUnderlineLabelTextColor = self.trailingUnderlineLabelTextColor;
copy.activeColor = self.activeColor;
copy.disabledColor = self.disabledColor;
copy.normalColor = self.normalColor;
return copy;
}
- (void)dealloc {
[self unsubscribeFromNotifications];
[self unsubscribeFromKVO];
}
- (void)commonMDCTextInputControllerLegacyFullWidthInitialization {
_characterCountViewMode = UITextFieldViewModeAlways;
_internalCharacterCounter = [[MDCTextInputAllCharactersCounter alloc] init];
}
- (void)setupInput {
if (!_textInput) {
return;
}
// This controller will handle Dynamic Type and all fonts for the text input
_mdc_adjustsFontForContentSizeCategory =
_textInput.mdc_adjustsFontForContentSizeCategory ||
[self class].mdc_adjustsFontForContentSizeCategoryDefault;
_textInput.mdc_adjustsFontForContentSizeCategory = NO;
_textInput.positioningDelegate = self;
[self setupClearButton];
[self subscribeForNotifications];
[self subscribeForKVO];
_textInput.underline.color = [UIColor clearColor];
[self updateLayout];
}
- (void)setupClearButton {
UIImage *image = [self
drawnClearButtonImage:[UIColor colorWithWhite:0 alpha:[MDCTypography captionFontOpacity]]];
[_textInput.clearButton setImage:image forState:UIControlStateNormal];
[_textInput.clearButton setImage:image forState:UIControlStateNormal];
[_textInput.clearButton setImage:image forState:UIControlStateNormal];
}
- (void)subscribeForNotifications {
if (!_textInput) {
return;
}
NSNotificationCenter *defaultCenter = [NSNotificationCenter defaultCenter];
if ([_textInput isKindOfClass:[UITextField class]]) {
[defaultCenter addObserver:self
selector:@selector(textInputDidBeginEditing:)
name:UITextFieldTextDidBeginEditingNotification
object:_textInput];
[defaultCenter addObserver:self
selector:@selector(textInputDidChange:)
name:UITextFieldTextDidChangeNotification
object:_textInput];
[defaultCenter addObserver:self
selector:@selector(textInputDidEndEditing:)
name:UITextFieldTextDidEndEditingNotification
object:_textInput];
[defaultCenter addObserver:self
selector:@selector(textInputDidChange:)
name:MDCTextFieldTextDidSetTextNotification
object:_textInput];
}
if ([_textInput isKindOfClass:[MDCMultilineTextField class]]) {
MDCMultilineTextField *textField = (MDCMultilineTextField *)_textInput;
[defaultCenter addObserver:self
selector:@selector(textInputDidBeginEditing:)
name:UITextViewTextDidBeginEditingNotification
object:textField.textView];
[defaultCenter addObserver:self
selector:@selector(textInputDidChange:)
name:UITextViewTextDidChangeNotification
object:textField.textView];
[defaultCenter addObserver:self
selector:@selector(textInputDidEndEditing:)
name:UITextViewTextDidEndEditingNotification
object:textField.textView];
}
}
- (void)unsubscribeFromNotifications {
NSNotificationCenter *defaultCenter = [NSNotificationCenter defaultCenter];
[defaultCenter removeObserver:self];
}
- (void)subscribeForKVO {
if (!_textInput) {
return;
}
[_textInput.placeholderLabel addObserver:self
forKeyPath:MDCTextInputControllerLegacyFullWidthKVOKeyFont
options:0
context:nil];
[_textInput.trailingUnderlineLabel addObserver:self
forKeyPath:MDCTextInputControllerLegacyFullWidthKVOKeyFont
options:0
context:nil];
_isRegisteredForKVO = YES;
}
- (void)unsubscribeFromKVO {
if (!self.textInput || !self.isRegisteredForKVO) {
return;
}
@try {
[self.textInput.placeholderLabel
removeObserver:self
forKeyPath:MDCTextInputControllerLegacyFullWidthKVOKeyFont];
[self.textInput.trailingUnderlineLabel
removeObserver:self
forKeyPath:MDCTextInputControllerLegacyFullWidthKVOKeyFont];
} @catch (__unused NSException *exception) {
}
_isRegisteredForKVO = NO;
}
#pragma mark - Character Max Implementation
- (NSUInteger)characterCount {
return [self.characterCounter characterCountForTextInput:self.textInput];
}
- (id<MDCTextInputCharacterCounter>)characterCounter {
if (!_characterCounter) {
_characterCounter = self.internalCharacterCounter;
}
return _characterCounter;
}
- (void)setCharacterCounter:(id<MDCTextInputCharacterCounter>)characterCounter {
if (_characterCounter != characterCounter) {
_characterCounter = characterCounter;
[self updateLayout];
}
}
- (void)setCharacterCountMax:(NSUInteger)characterCountMax {
if (_characterCountMax != characterCountMax) {
_characterCountMax = characterCountMax;
[self updateLayout];
}
}
#pragma mark - Clear Button Customization
- (UIImage *)drawnClearButtonImage:(UIColor *)color {
CGSize clearButtonSize =
CGSizeMake(MDCTextInputControllerLegacyFullWidthClearButtonImageSquareWidthHeight,
MDCTextInputControllerLegacyFullWidthClearButtonImageSquareWidthHeight);
CGFloat scale = [UIScreen mainScreen].scale;
CGRect bounds = CGRectMake(0, 0, clearButtonSize.width * scale, clearButtonSize.height * scale);
UIGraphicsBeginImageContextWithOptions(bounds.size, false, scale);
[color setFill];
[MDCPathForClearButtonLegacyImageFrame(bounds) fill];
UIImage *image = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
image = [image imageWithRenderingMode:UIImageRenderingModeAlwaysTemplate];
return image;
}
#pragma mark - Leading Label Customization
- (void)updateLeadingUnderlineLabel {
self.textInput.leadingUnderlineLabel.text = nil;
self.textInput.leadingUnderlineLabel.textColor = self.leadingUnderlineLabelTextColor;
}
#pragma mark - Placeholder Customization
- (void)updatePlaceholder {
if (!self.customPlaceholderFont) {
self.textInput.placeholderLabel.font = [[self class] placeholderFont];
}
self.textInput.placeholderLabel.textColor = self.inlinePlaceholderColor;
}
#pragma mark - Trailing Label Customization
- (void)updateTrailingUnderlineLabel {
if (!self.characterCountMax) {
self.textInput.trailingUnderlineLabel.text = nil;
} else {
self.textInput.trailingUnderlineLabel.text = [self characterCountText];
if (!self.customTrailingFont) {
self.textInput.trailingUnderlineLabel.font = [[self class] underlineLabelsFont];
}
}
UIColor *textColor = self.trailingUnderlineLabelTextColor;
if (self.isDisplayingCharacterCountError || self.isDisplayingErrorText) {
textColor = self.errorColor;
}
switch (self.characterCountViewMode) {
case UITextFieldViewModeAlways:
break;
case UITextFieldViewModeWhileEditing:
textColor = !self.textInput.isEditing ? [UIColor clearColor] : textColor;
break;
case UITextFieldViewModeUnlessEditing:
textColor = self.textInput.isEditing ? [UIColor clearColor] : textColor;
break;
case UITextFieldViewModeNever:
textColor = [UIColor clearColor];
break;
}
self.textInput.trailingUnderlineLabel.textColor = textColor;
}
- (NSString *)characterCountText {
// TODO: (larche) Localize
return [NSString stringWithFormat:@"%lu / %lu", (unsigned long)[self characterCount],
(unsigned long)self.characterCountMax];
}
#pragma mark - Underline Customization
- (void)updateUnderline {
// Hide the underline.
self.textInput.underline.color = [UIColor clearColor];
}
#pragma mark - Underline Labels Fonts
+ (UIFont *)placeholderFont {
return [UIFont mdc_preferredFontForMaterialTextStyle:MDCFontTextStyleBody1];
}
+ (UIFont *)underlineLabelsFont {
return [UIFont mdc_preferredFontForMaterialTextStyle:MDCFontTextStyleCaption];
}
#pragma mark - Properties Implementation
// The underline is never shown in this style.
- (void)setActiveColor:(__unused UIColor *)activeColor {
[self updateUnderline];
}
- (UIColor *)activeColor {
return [UIColor clearColor];
}
+ (UIColor *)activeColorDefault {
return [UIColor clearColor];
}
+ (void)setActiveColorDefault:(__unused UIColor *)activeColorDefault {
// Not implemented. Underline is always clear.
}
- (void)setCharacterCountViewMode:(UITextFieldViewMode)characterCountViewMode {
if (_characterCountViewMode != characterCountViewMode) {
_characterCountViewMode = characterCountViewMode;
[self updateLayout];
}
}
- (UIRectCorner)roundedCorners {
return 0;
}
- (void)setRoundedCorners:(__unused UIRectCorner)roundedCorners {
// Not implemented. There are no corners to round.
}
+ (UIRectCorner)roundedCornersDefault {
return 0;
}
+ (void)setRoundedCornersDefault:(__unused UIRectCorner)roundedCornersDefault {
// Not implemented. There are no corners to round.
}
- (void)setDisabledColor:(__unused UIColor *)disabledColor {
[self updateUnderline];
}
- (UIColor *)disabledColor {
return [UIColor clearColor];
}
+ (void)setDisabledColorDefault:(__unused UIColor *)disabledColorDefault {
// This controller does not have decorations that need to change for a disabled state.
}
+ (UIColor *)disabledColorDefault {
return [UIColor clearColor];
}
- (BOOL)isDisplayingCharacterCountError {
return self.characterCountMax && [self characterCount] > self.characterCountMax;
}
- (BOOL)isDisplayingErrorText {
return self.errorText != nil;
}
- (void)setErrorAccessibilityValue:(NSString *)errorAccessibilityValue {
_errorAccessibilityValue = [errorAccessibilityValue copy];
}
- (UIColor *)errorColor {
if (!_errorColor) {
_errorColor = [self class].errorColorDefault;
}
return _errorColor;
}
- (void)setErrorColor:(UIColor *)errorColor {
if (![_errorColor isEqual:errorColor]) {
_errorColor = errorColor ? errorColor : [self class].errorColorDefault;
if (self.isDisplayingCharacterCountError || self.isDisplayingErrorText) {
[self updateLeadingUnderlineLabel];
[self updatePlaceholder];
[self updateTrailingUnderlineLabel];
[self updateUnderline];
}
}
}
+ (UIColor *)errorColorDefault {
if (!_errorColorDefault) {
_errorColorDefault = MDCTextInputControllerLegacyFullWidthErrorColorDefault();
}
return _errorColorDefault;
}
+ (void)setErrorColorDefault:(UIColor *)errorColorDefault {
_errorColorDefault = errorColorDefault ? errorColorDefault
: MDCTextInputControllerLegacyFullWidthErrorColorDefault();
}
- (void)setErrorText:(NSString *)errorText {
_errorText = [errorText copy];
}
- (void)setHelperText:(NSString *)helperText {
if (self.isDisplayingErrorText) {
self.previousLeadingText = helperText;
} else {
if (![self.textInput.leadingUnderlineLabel.text isEqualToString:helperText]) {
self.textInput.leadingUnderlineLabel.text = helperText;
[self updateLayout];
}
}
}
- (NSString *)helperText {
if (self.isDisplayingErrorText) {
return self.previousLeadingText;
} else {
return self.textInput.leadingUnderlineLabel.text;
}
}
- (void)setInlinePlaceholderColor:(UIColor *)inlinePlaceholderColor {
if (![_inlinePlaceholderColor isEqual:inlinePlaceholderColor]) {
_inlinePlaceholderColor = inlinePlaceholderColor;
[self updatePlaceholder];
}
}
- (UIColor *)inlinePlaceholderColor {
return _inlinePlaceholderColor ? _inlinePlaceholderColor
: [self class].inlinePlaceholderColorDefault;
}
+ (UIColor *)inlinePlaceholderColorDefault {
if (!_inlinePlaceholderColorDefault) {
_inlinePlaceholderColorDefault =
MDCTextInputControllerLegacyFullWidthInlinePlaceholderTextColorDefault();
}
return _inlinePlaceholderColorDefault;
}
+ (void)setInlinePlaceholderColorDefault:(UIColor *)inlinePlaceholderColorDefault {
_inlinePlaceholderColorDefault =
inlinePlaceholderColorDefault
? inlinePlaceholderColorDefault
: MDCTextInputControllerLegacyFullWidthInlinePlaceholderTextColorDefault();
}
// In This style, the leading underline is not shown. It would overlap the placeholder.
- (UIColor *)leadingUnderlineLabelTextColor {
return [UIColor clearColor];
}
- (void)setLeadingUnderlineLabelTextColor:(__unused UIColor *)leadingUnderlineLabelTextColor {
// Not implemented. Leading underline label is always clear.
}
// In This style, the leading underline is not shown. It would overlap the placeholder.
+ (UIColor *)leadingUnderlineLabelTextColorDefault {
return [UIColor clearColor];
}
+ (void)setLeadingUnderlineLabelTextColorDefault:
(__unused UIColor *)leadingUnderlineLabelTextColorDefault {
// Not implemented. Leading underline label is always clear.
}
// The underline is never shown in this style.
- (void)setNormalColor:(__unused UIColor *)normalColor {
[self updateUnderline];
}
- (UIColor *)normalColor {
return [UIColor clearColor];
}
+ (void)setNormalColorDefault:(__unused UIColor *)normalColorDefault {
// Not implemented. Underline is always clear.
}
+ (UIColor *)normalColorDefault {
return [UIColor clearColor];
}
- (void)setPreviousLeadingText:(NSString *)previousLeadingText {
_previousLeadingText = [previousLeadingText copy];
}
- (void)setPreviousPlaceholderColor:(UIColor *)previousPlaceholderColor {
_previousPlaceholderColor = previousPlaceholderColor;
}
- (void)setTextInput:(UIView<MDCTextInput> *)textInput {
if (_textInput != textInput) {
[self unsubscribeFromNotifications];
[self unsubscribeFromKVO];
_textInput = textInput;
[self setupInput];
}
}
- (UIColor *)trailingUnderlineLabelTextColor {
return _trailingUnderlineLabelTextColor ? _trailingUnderlineLabelTextColor
: [self class].trailingUnderlineLabelTextColorDefault;
}
- (void)setTrailingUnderlineLabelTextColor:(UIColor *)trailingUnderlineLabelTextColor {
if (_trailingUnderlineLabelTextColor != trailingUnderlineLabelTextColor) {
_trailingUnderlineLabelTextColor = trailingUnderlineLabelTextColor
? trailingUnderlineLabelTextColor
: [self class].trailingUnderlineLabelTextColorDefault;
[self updateTrailingUnderlineLabel];
}
}
+ (UIColor *)trailingUnderlineLabelTextColorDefault {
if (!_trailingUnderlineLabelTextColorDefault) {
_trailingUnderlineLabelTextColorDefault =
MDCTextInputControllerLegacyFullWidthInlinePlaceholderTextColorDefault();
}
return _trailingUnderlineLabelTextColorDefault;
}
+ (void)setTrailingUnderlineLabelTextColorDefault:
(UIColor *)trailingUnderlineLabelTextColorDefault {
_trailingUnderlineLabelTextColorDefault =
trailingUnderlineLabelTextColorDefault
? trailingUnderlineLabelTextColorDefault
: MDCTextInputControllerLegacyFullWidthInlinePlaceholderTextColorDefault();
}
- (void)setUnderlineViewMode:(__unused UITextFieldViewMode)underlineViewMode {
[self updateLayout];
}
- (UITextFieldViewMode)underlineViewMode {
return UITextFieldViewModeNever;
}
+ (UITextFieldViewMode)underlineViewModeDefault {
return UITextFieldViewModeNever;
}
+ (void)setUnderlineViewModeDefault:(__unused UITextFieldViewMode)underlineViewModeDefault {
// Not implemented. Underline is never shown.
}
#pragma mark - Layout
- (void)updateLayout {
if (!_textInput) {
return;
}
[self updatePlaceholder];
[self updateLeadingUnderlineLabel];
[self updateTrailingUnderlineLabel];
[self updateUnderline];
[self updateConstraints];
}
- (void)updateConstraints {
if (!self.characterCountTrailing) {
self.characterCountTrailing = [NSLayoutConstraint
constraintWithItem:self.textInput.trailingUnderlineLabel
attribute:NSLayoutAttributeTrailing
relatedBy:NSLayoutRelationEqual
toItem:self.textInput
attribute:NSLayoutAttributeTrailing
multiplier:1
constant:-1 * MDCTextInputControllerLegacyFullWidthHorizontalPadding];
}
if (!self.clearButtonTrailingCharacterCountLeading) {
self.clearButtonTrailingCharacterCountLeading =
[NSLayoutConstraint constraintWithItem:self.textInput.clearButton
attribute:NSLayoutAttributeTrailing
relatedBy:NSLayoutRelationEqual
toItem:self.textInput.trailingUnderlineLabel
attribute:NSLayoutAttributeLeading
multiplier:1
constant:0];
}
if (!self.placeholderLeading) {
self.placeholderLeading = [NSLayoutConstraint
constraintWithItem:self.textInput.placeholderLabel
attribute:NSLayoutAttributeLeading
relatedBy:NSLayoutRelationEqual
toItem:self.textInput
attribute:NSLayoutAttributeLeading
multiplier:1
constant:MDCTextInputControllerLegacyFullWidthHorizontalPadding];
}
if (!self.placeholderTrailingCharacterCountLeading) {
self.placeholderTrailingCharacterCountLeading = [NSLayoutConstraint
constraintWithItem:self.textInput.placeholderLabel
attribute:NSLayoutAttributeTrailing
relatedBy:NSLayoutRelationLessThanOrEqual
toItem:self.textInput.trailingUnderlineLabel
attribute:NSLayoutAttributeLeading
multiplier:1
constant:-1 * MDCTextInputControllerLegacyFullWidthHorizontalInnerPadding];
}
if (!self.placeholderTrailingSuperviewTrailing) {
self.placeholderTrailingSuperviewTrailing = [NSLayoutConstraint
constraintWithItem:self.textInput.placeholderLabel
attribute:NSLayoutAttributeTrailing
relatedBy:NSLayoutRelationLessThanOrEqual
toItem:self.textInput
attribute:NSLayoutAttributeTrailing
multiplier:1
constant:-1 * MDCTextInputControllerLegacyFullWidthHorizontalPadding];
}
// Multi Line Only
if ([self.textInput isKindOfClass:[MDCMultilineTextField class]]) {
[self.textInput.leadingUnderlineLabel setContentHuggingPriority:UILayoutPriorityRequired
forAxis:UILayoutConstraintAxisVertical];
[self.textInput.leadingUnderlineLabel
setContentCompressionResistancePriority:UILayoutPriorityRequired
forAxis:UILayoutConstraintAxisVertical];
[self.textInput.trailingUnderlineLabel
setContentCompressionResistancePriority:UILayoutPriorityRequired
forAxis:UILayoutConstraintAxisVertical];
if (!self.characterCountY) {
self.characterCountY =
[NSLayoutConstraint constraintWithItem:self.textInput.trailingUnderlineLabel
attribute:NSLayoutAttributeBottom
relatedBy:NSLayoutRelationEqual
toItem:((MDCMultilineTextField *)self.textInput).textView
attribute:NSLayoutAttributeBottom
multiplier:1
constant:0];
}
if (!self.clearButtonY) {
self.clearButtonY =
[NSLayoutConstraint constraintWithItem:self.textInput.clearButton
attribute:NSLayoutAttributeCenterY
relatedBy:NSLayoutRelationEqual
toItem:self.textInput.trailingUnderlineLabel
attribute:NSLayoutAttributeCenterY
multiplier:1
constant:0];
}
if (!self.multilineTextViewBottom) {
self.multilineTextViewBottom = [NSLayoutConstraint
constraintWithItem:((MDCMultilineTextField *)self.textInput).textView
attribute:NSLayoutAttributeBottom
relatedBy:NSLayoutRelationEqual
toItem:self.textInput
attribute:NSLayoutAttributeBottom
multiplier:1
constant:-1 * MDCTextInputControllerLegacyFullWidthVerticalPadding];
}
if (!self.multilineTextViewTop) {
self.multilineTextViewTop = [NSLayoutConstraint
constraintWithItem:((MDCMultilineTextField *)self.textInput).textView
attribute:NSLayoutAttributeTop
relatedBy:NSLayoutRelationEqual
toItem:self.textInput
attribute:NSLayoutAttributeTop
multiplier:1
constant:MDCTextInputControllerLegacyFullWidthVerticalPadding];
}
if (!self.multilinePlaceholderCenterY) {
self.multilinePlaceholderCenterY =
[NSLayoutConstraint constraintWithItem:self.textInput.placeholderLabel
attribute:NSLayoutAttributeCenterY
relatedBy:NSLayoutRelationEqual
toItem:((MDCMultilineTextField *)self.textInput).textView
attribute:NSLayoutAttributeCenterY
multiplier:1
constant:0];
}
CGFloat scale = UIScreen.mainScreen.scale;
CGFloat characterCountHeightConstant =
MDCCeil(((MDCMultilineTextField *)self.textInput).textView.font.lineHeight * scale) / scale;
if (!self.multilineCharacterCountHeight) {
self.multilineCharacterCountHeight =
[NSLayoutConstraint constraintWithItem:self.textInput.trailingUnderlineLabel
attribute:NSLayoutAttributeHeight
relatedBy:NSLayoutRelationEqual
toItem:nil
attribute:NSLayoutAttributeNotAnAttribute
multiplier:1
constant:characterCountHeightConstant];
}
self.multilineCharacterCountHeight.constant = characterCountHeightConstant;
[NSLayoutConstraint activateConstraints:@[
self.multilineTextViewBottom, self.multilineTextViewTop, self.multilinePlaceholderCenterY,
self.multilineCharacterCountHeight
]];
// A height constraint is not necessary for multiline. Its height is calculated in
// intrinsicContentSize:
} else {
// Single Line Only
// .fullWidth
if (!self.characterCountY) {
self.characterCountY =
[NSLayoutConstraint constraintWithItem:self.textInput.trailingUnderlineLabel
attribute:NSLayoutAttributeCenterY
relatedBy:NSLayoutRelationEqual
toItem:self.textInput
attribute:NSLayoutAttributeCenterY
multiplier:1
constant:0];
}
if (!self.clearButtonY) {
self.clearButtonY = [NSLayoutConstraint constraintWithItem:self.textInput.clearButton
attribute:NSLayoutAttributeCenterY
relatedBy:NSLayoutRelationEqual
toItem:self.textInput
attribute:NSLayoutAttributeCenterY
multiplier:1
constant:0];
}
}
[NSLayoutConstraint activateConstraints:@[
self.characterCountY, self.characterCountTrailing,
self.clearButtonTrailingCharacterCountLeading, self.clearButtonY, self.placeholderLeading,
self.placeholderTrailingCharacterCountLeading, self.placeholderTrailingSuperviewTrailing
]];
[self.textInput.trailingUnderlineLabel setContentHuggingPriority:UILayoutPriorityRequired
forAxis:UILayoutConstraintAxisVertical];
}
- (void)updateFontsForDynamicType {
if (self.mdc_adjustsFontForContentSizeCategory) {
UIFont *textFont = [UIFont mdc_preferredFontForMaterialTextStyle:MDCFontTextStyleBody1];
self.textInput.font = textFont;
[self updateLayout];
}
}
#pragma mark - MDCTextFieldPositioningDelegate
// clang-format off
/**
textInsets: is the source of truth for vertical layout. It's used to figure out the proper
height and also where to place the placeholder / text field.
NOTE: It's applied before the textRect is flipped for RTL. So all calculations are done here à la
LTR.
The vertical layout is, at most complex, this form:
MDCTextInputVerticalPadding + // Top padding
MDCRint(self.textInput.placeholderLabel.font.lineHeight * scale) + // Placeholder when up
MDCTextInputVerticalHalfPadding + // Small padding
MDCRint(MAX(self.textInput.font.lineHeight, // Text field or placeholder
self.textInput.placeholderLabel.font.lineHeight)) +
MDCTextInputVerticalHalfPadding + // Small padding
--Underline-- (height not counted) // Underline (height ignored)
MAX(underlineLabelsOffset,MDCTextInputVerticalHalfPadding) // Padding and/or labels
*/
// clang-format on
- (UIEdgeInsets)textInsets:(__unused UIEdgeInsets)defaultInsets {
// NOTE: UITextFields have a centerY based layout. But you can change EITHER the height or the Y.
// Not both. Don't know why. So, we have to leave the text rect as big as the bounds and move it
// to a Y that works. In other words, no bottom inset will make a difference here for UITextFields
UIEdgeInsets textInsets = UIEdgeInsetsZero;
textInsets.top = MDCTextInputControllerLegacyFullWidthVerticalPadding;
textInsets.bottom = MDCTextInputControllerLegacyFullWidthVerticalPadding;
textInsets.left = MDCTextInputControllerLegacyFullWidthHorizontalPadding;
textInsets.right = MDCTextInputControllerLegacyFullWidthHorizontalPadding;
// The trailing label gets in the way. If it has a frame, it's used. But if not, an
// estimate is made of the size the text will be.
if (CGRectGetWidth(self.textInput.trailingUnderlineLabel.frame) > 1.f) {
textInsets.right += MDCCeil(CGRectGetWidth(self.textInput.trailingUnderlineLabel.frame));
} else if (self.characterCountMax) {
CGRect charCountRect = [[self characterCountText]
boundingRectWithSize:self.textInput.bounds.size
options:NSStringDrawingUsesLineFragmentOrigin
attributes:@{
NSFontAttributeName : self.textInput.trailingUnderlineLabel.font
}
context:nil];
textInsets.right += MDCCeil(CGRectGetWidth(charCountRect));
}
return textInsets;
}
- (CGRect)editingRectForBounds:(__unused CGRect)bounds defaultRect:(CGRect)defaultRect {
if (![self.textInput isKindOfClass:[UITextField class]]) {
return CGRectZero;
}
MDCTextField *textField = (MDCTextField *)self.textInput;
CGRect editingRect = defaultRect;
// Full width text fields have their clear button in the horizontal margin, but because the
// internal implementation of textRect calls [super clearButtonRectForBounds:] in its
// implementation, our modifications are not picked up. Adjust accordingly.
// Full width text boxes have their character count on the text input line
if (self.textInput.text.length > 0) {
switch (textField.clearButtonMode) {
case UITextFieldViewModeWhileEditing:
editingRect.size.width -= CGRectGetWidth(self.textInput.clearButton.bounds);
case UITextFieldViewModeUnlessEditing:
// The 'defaultRect' is based on the textInsets so we need to compensate for
// the button NOT being there.
editingRect.size.width += CGRectGetWidth(self.textInput.clearButton.bounds);
editingRect.size.width -= MDCTextInputControllerLegacyFullWidthHorizontalInnerPadding;
break;
default:
break;
}
}
return editingRect;
}
#pragma mark - UITextField & UITextView Notification Observation
- (void)textInputDidBeginEditing:(__unused NSNotification *)note {
[self updateLayout];
if (self.characterCountMax > 0) {
NSString *announcementString;
if (!announcementString.length) {
announcementString = [NSString
stringWithFormat:@"%lu character limit.", (unsigned long)self.characterCountMax];
}
// Simply sending a layout change notification does not seem to
UIAccessibilityPostNotification(UIAccessibilityAnnouncementNotification, announcementString);
}
}
- (void)textInputDidChange:(__unused NSNotification *)note {
[self updateLayout];
// Accessibility
if (self.textInput.isEditing && self.characterCountMax > 0) {
NSString *announcementString;
if (!announcementString.length) {
announcementString = [NSString
stringWithFormat:@"%lu characters remaining",
(unsigned long)(self.characterCountMax -
[self.characterCounter
characterCountForTextInput:self.textInput])];
}
// Simply sending a layout change notification does not seem to
UIAccessibilityPostNotification(UIAccessibilityAnnouncementNotification, announcementString);
}
}
- (void)textInputDidEndEditing:(__unused NSNotification *)note {
[self updateLayout];
}
#pragma mark - KVO
- (void)observeValueForKeyPath:(NSString *)keyPath
ofObject:(id)object
change:(__unused NSDictionary<NSKeyValueChangeKey, id> *)change
context:(__unused void *)context {
// Listening to outside setting of custom fonts.
if (![keyPath isEqualToString:MDCTextInputControllerLegacyFullWidthKVOKeyFont]) {
return;
}
if (object == _textInput.placeholderLabel &&
![_textInput.placeholderLabel.font isEqual:[[self class] placeholderFont]]) {
_customPlaceholderFont = _textInput.placeholderLabel.font;
} else if (object == _textInput.trailingUnderlineLabel &&
![_textInput.trailingUnderlineLabel.font isEqual:[[self class] underlineLabelsFont]]) {
_customTrailingFont = _textInput.trailingUnderlineLabel.font;
} else {
return;
}
[self updateLayout];
}
#pragma mark - Public API
- (void)setErrorText:(NSString *)errorText
errorAccessibilityValue:(NSString *)errorAccessibilityValue {
// Turn on error:
//
// Here the 'magic' logic happens for error text.
// When the user sets error text, we save the current state of their underline, leading text,
// trailing text, and placeholder text for both content and color.
if (errorText && !self.isDisplayingErrorText) {
// If we are not in error, but will be, we need to save the existing state.
self.previousLeadingText = self.textInput.leadingUnderlineLabel.text
? self.textInput.leadingUnderlineLabel.text.copy
: @"";
self.textInput.leadingUnderlineLabel.text = errorText;
[self updatePlaceholder];
}
// Change error:
if (errorText && self.isDisplayingErrorText) {
self.textInput.leadingUnderlineLabel.text = errorText;
}
// Turn off error:
//
// If error text is unset (nil) we reset to previous values.
if (!errorText) {
// If there is a saved state, use it.
if (self.previousLeadingText) {
self.textInput.leadingUnderlineLabel.text = self.previousLeadingText;
}
// Clear out saved state.
self.previousLeadingText = nil;
}
self.errorText = errorText;
self.errorAccessibilityValue = errorAccessibilityValue;
[self updateLayout];
// Accessibility
// TODO: (larche) Localize
if (errorText) {
NSString *announcementString =
errorText.length > 0 ? [NSString stringWithFormat:@"Error: %@", errorText] : @"Error.";
// Simply sending a layout change notification does not seem to
UIAccessibilityPostNotification(UIAccessibilityAnnouncementNotification, announcementString);
NSString *valueString = @"";
if (self.textInput.text.length > 0) {
valueString = [self.textInput.text copy];
}
if (self.textInput.placeholder.length > 0) {
valueString = [NSString stringWithFormat:@"%@. %@", valueString, self.textInput.placeholder];
}
valueString = [valueString stringByAppendingString:@"."];
self.textInput.accessibilityValue = valueString;
NSString *leadingUnderlineLabelText = self.textInput.leadingUnderlineLabel.text;
self.textInput.leadingUnderlineLabel.accessibilityLabel =
[NSString stringWithFormat:@"Error: %@.",
leadingUnderlineLabelText ? leadingUnderlineLabelText : @""];
} else {
self.textInput.accessibilityValue = nil;
self.textInput.leadingUnderlineLabel.accessibilityLabel = nil;
}
}
#pragma mark - Accessibility
- (BOOL)mdc_adjustsFontForContentSizeCategory {
return _mdc_adjustsFontForContentSizeCategory;
}
- (void)mdc_setAdjustsFontForContentSizeCategory:(BOOL)adjusts {
_mdc_adjustsFontForContentSizeCategory = adjusts;
if (_mdc_adjustsFontForContentSizeCategory) {
[self updateFontsForDynamicType];
}
if (_mdc_adjustsFontForContentSizeCategory) {
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(contentSizeCategoryDidChange:)
name:UIContentSizeCategoryDidChangeNotification
object:nil];
} else {
[[NSNotificationCenter defaultCenter] removeObserver:self
name:UIContentSizeCategoryDidChangeNotification
object:nil];
}
}
+ (BOOL)mdc_adjustsFontForContentSizeCategoryDefault {
return _mdc_adjustsFontForContentSizeCategoryDefault;
}
+ (void)setMdc_adjustsFontForContentSizeCategoryDefault:
(BOOL)mdc_adjustsFontForContentSizeCategoryDefault {
_mdc_adjustsFontForContentSizeCategoryDefault = mdc_adjustsFontForContentSizeCategoryDefault;
}
- (void)contentSizeCategoryDidChange:(__unused NSNotification *)notification {
[self updateFontsForDynamicType];
}
@end