BTUIKFormField.m 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371
  1. #import "BTUIKFormField.h"
  2. #import "BTUIKVectorArtView.h"
  3. #import "BTUIKViewUtil.h"
  4. #import "BTUIKAppearance.h"
  5. @interface BTUIKFormField ()<BTUIKTextFieldEditDelegate>
  6. @property (nonatomic, copy) NSString *previousTextFieldText;
  7. @property (nonatomic, strong) NSMutableArray *layoutConstraints;
  8. @end
  9. @implementation BTUIKFormField
  10. - (instancetype)initWithFrame:(CGRect)frame {
  11. self = [super initWithFrame:frame];
  12. if (self) {
  13. self.backgroundColor = [BTUIKAppearance sharedInstance].formFieldBackgroundColor;
  14. self.translatesAutoresizingMaskIntoConstraints = NO;
  15. self.displayAsValid = YES;
  16. BTUIKTextField *textField = [BTUIKTextField new];
  17. textField.editDelegate = self;
  18. _textField = textField;
  19. self.textField.translatesAutoresizingMaskIntoConstraints = NO;
  20. self.textField.borderStyle = UITextBorderStyleNone;
  21. self.textField.backgroundColor = [UIColor clearColor];
  22. self.textField.opaque = NO;
  23. self.textField.adjustsFontSizeToFitWidth = YES;
  24. self.textField.returnKeyType = UIReturnKeyNext;
  25. self.textField.keyboardAppearance = [BTUIKAppearance sharedInstance].keyboardAppearance;
  26. [self.textField addTarget:self action:@selector(fieldContentDidChange) forControlEvents:UIControlEventEditingChanged];
  27. [self.textField addTarget:self action:@selector(editingDidBegin) forControlEvents:UIControlEventEditingDidBegin];
  28. [self.textField addTarget:self action:@selector(editingDidEnd) forControlEvents:UIControlEventEditingDidEnd];
  29. self.textField.delegate = self;
  30. [self addSubview:self.textField];
  31. self.formLabel = [[UILabel alloc] init];
  32. [BTUIKAppearance styleLabelBoldPrimary:self.formLabel];
  33. self.formLabel.translatesAutoresizingMaskIntoConstraints = NO;
  34. self.formLabel.text = @"";
  35. self.formLabel.accessibilityElementsHidden = YES;
  36. [self addSubview:self.formLabel];
  37. [self.formLabel setContentHuggingPriority:UILayoutPriorityRequired forAxis:UILayoutConstraintAxisHorizontal];
  38. [self.formLabel setContentCompressionResistancePriority:UILayoutPriorityRequired forAxis:UILayoutConstraintAxisHorizontal];
  39. [self.textField setContentCompressionResistancePriority:UILayoutPriorityDefaultLow forAxis:UILayoutConstraintAxisHorizontal];
  40. [self addGestureRecognizer:[[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(tappedField)]];
  41. [self setContentHuggingPriority:UILayoutPriorityRequired forAxis:UILayoutConstraintAxisVertical];
  42. self.opaque = NO;
  43. [self updateConstraints];
  44. }
  45. return self;
  46. }
  47. - (void)updateConstraints {
  48. if (self.layoutConstraints != nil) {
  49. [self removeConstraints:self.layoutConstraints];
  50. }
  51. self.layoutConstraints = [NSMutableArray array];
  52. NSMutableDictionary* viewBindings = [@{@"view":self, @"textField":self.textField, @"formLabel": self.formLabel} mutableCopy];
  53. if (self.accessoryView) {
  54. viewBindings[@"accessoryView"] = self.accessoryView;
  55. }
  56. NSDictionary* metrics = @{@"PADDING":@15};
  57. BOOL hasFormLabel = (self.formLabel.text.length > 0);
  58. [self.layoutConstraints addObjectsFromArray:[NSLayoutConstraint constraintsWithVisualFormat:@"V:|[textField]|"
  59. options:0
  60. metrics:metrics
  61. views:viewBindings]];
  62. [self.layoutConstraints addObjectsFromArray:[NSLayoutConstraint constraintsWithVisualFormat:@"V:|[formLabel]|"
  63. options:0
  64. metrics:metrics
  65. views:viewBindings]];
  66. if (hasFormLabel) {
  67. [self.layoutConstraints addObjectsFromArray:[NSLayoutConstraint constraintsWithVisualFormat:@"H:|-(PADDING)-[formLabel(<=0@1)]-[textField]"
  68. options:0
  69. metrics:metrics
  70. views:viewBindings]];
  71. } else {
  72. [self.layoutConstraints addObjectsFromArray:[NSLayoutConstraint constraintsWithVisualFormat:@"H:|-(PADDING)-[textField]"
  73. options:0
  74. metrics:metrics
  75. views:viewBindings]];
  76. }
  77. if (self.accessoryView && !self.accessoryView.hidden) {
  78. [self.layoutConstraints addObjectsFromArray:@[[NSLayoutConstraint constraintWithItem:self.accessoryView
  79. attribute:NSLayoutAttributeCenterY
  80. relatedBy:NSLayoutRelationEqual
  81. toItem:self
  82. attribute:NSLayoutAttributeCenterY
  83. multiplier:1.0f
  84. constant:0.0f]]];
  85. ;
  86. [self.layoutConstraints addObjectsFromArray:[NSLayoutConstraint constraintsWithVisualFormat:@"H:[textField]-[accessoryView]-(PADDING)-|"
  87. options:0
  88. metrics:metrics
  89. views:viewBindings]];
  90. } else {
  91. [self.layoutConstraints addObjectsFromArray:[NSLayoutConstraint constraintsWithVisualFormat:@"H:[textField]-(PADDING)-|"
  92. options:0
  93. metrics:metrics
  94. views:viewBindings]];
  95. }
  96. NSArray *contraintsToAdd = [self.layoutConstraints copy];
  97. [self addConstraints:contraintsToAdd];
  98. NSTextAlignment newAlignment = hasFormLabel ? [BTUIKViewUtil naturalTextAlignmentInverse] : [BTUIKViewUtil naturalTextAlignment];
  99. if (newAlignment != self.textField.textAlignment) {
  100. self.textField.textAlignment = newAlignment;
  101. }
  102. [super updateConstraints];
  103. }
  104. - (void)textFieldDidBeginEditing:(__unused UITextField *)textField {
  105. if ([self.delegate respondsToSelector:@selector(formFieldDidBeginEditing:)]) {
  106. [self.delegate formFieldDidBeginEditing:self];
  107. }
  108. }
  109. - (void)textFieldDidEndEditing:(__unused UITextField *)textField {
  110. if ([self.delegate respondsToSelector:@selector(formFieldDidEndEditing:)]) {
  111. [self.delegate formFieldDidEndEditing:self];
  112. }
  113. }
  114. #pragma mark - Drawing
  115. - (void)drawRect:(CGRect)rect {
  116. // Draw borders
  117. [[BTUIKAppearance sharedInstance].lineColor setFill];
  118. CGContextRef context = UIGraphicsGetCurrentContext();
  119. if (!self.displayAsValid) {
  120. CGPathRef path = CGPathCreateWithRect(CGRectMake(rect.origin.x, CGRectGetMaxY(rect) - 0.5f, rect.size.width, 0.5f), NULL);
  121. CGContextAddPath(context, path);
  122. CGPathRelease(path);
  123. path = CGPathCreateWithRect(CGRectMake(rect.origin.x, 0, rect.size.width, 0.5f), NULL);
  124. CGContextAddPath(context, path);
  125. CGContextDrawPath(context, kCGPathFill);
  126. CGPathRelease(path);
  127. } else {
  128. if (self.interFieldBorder || self.bottomBorder) {
  129. CGFloat horizontalMargin = self.bottomBorder ? 0 : 17.0f;
  130. CGPathRef path = CGPathCreateWithRect(CGRectMake(rect.origin.x + horizontalMargin, CGRectGetMaxY(rect) - 0.5f, rect.size.width - horizontalMargin, 0.5f), NULL);
  131. CGContextAddPath(context, path);
  132. CGContextDrawPath(context, kCGPathFill);
  133. CGPathRelease(path);
  134. }
  135. if (self.topBorder) {
  136. CGPathRef path = CGPathCreateWithRect(CGRectMake(rect.origin.x, 0, rect.size.width, 0.5f), NULL);
  137. CGContextAddPath(context, path);
  138. CGContextDrawPath(context, kCGPathFill);
  139. CGPathRelease(path);
  140. }
  141. }
  142. }
  143. - (void)setBottomBorder:(BOOL)bottomBorder {
  144. _bottomBorder = bottomBorder;
  145. [self setNeedsDisplay];
  146. }
  147. - (void)setInterFieldBorder:(BOOL)interFieldBorder {
  148. _interFieldBorder = interFieldBorder;
  149. [self setNeedsDisplay];
  150. }
  151. - (void)setTopBorder:(BOOL)topBorder {
  152. _topBorder = topBorder;
  153. [self setNeedsDisplay];
  154. }
  155. - (void)updateAppearance {
  156. UIColor *textColor;
  157. NSString *currentAccessibilityLabel = self.textField.accessibilityLabel;
  158. if (!self.displayAsValid){
  159. textColor = [BTUIKAppearance sharedInstance].errorForegroundColor;
  160. if (currentAccessibilityLabel != nil) {
  161. self.textField.accessibilityLabel = [self addInvalidAccessibilityToString:currentAccessibilityLabel];
  162. }
  163. } else {
  164. textColor = [BTUIKAppearance sharedInstance].primaryTextColor;
  165. if (currentAccessibilityLabel != nil) {
  166. self.textField.accessibilityLabel = [self stripInvalidAccessibilityFromString:currentAccessibilityLabel];
  167. }
  168. }
  169. NSMutableAttributedString *mutableText = [[NSMutableAttributedString alloc] initWithAttributedString:self.textField.attributedText];
  170. [mutableText addAttributes:@{NSForegroundColorAttributeName: textColor,
  171. NSFontAttributeName:[[BTUIKAppearance sharedInstance].font fontWithSize:UIFont.labelFontSize]}
  172. range:NSMakeRange(0, mutableText.length)];
  173. UITextRange *currentRange = self.textField.selectedTextRange;
  174. self.textField.attributedText = mutableText;
  175. // Reassign current selection range, since it gets cleared after attributedText assignment
  176. self.textField.selectedTextRange = currentRange;
  177. }
  178. #pragma mark - BTUITextFieldEditDelegate methods
  179. - (void)textFieldWillDeleteBackward:(__unused BTUIKFormField *)textField {
  180. // _backspace indicates that the backspace key was typed.
  181. _backspace = YES;
  182. }
  183. - (void)textFieldDidDeleteBackward:(__unused BTUIKFormField *)textField originalText:(__unused NSString *)originalText {
  184. // To be implemented by subclasses
  185. }
  186. - (void)textField:(__unused BTUIKFormField *)textField willInsertText:(__unused NSString *)text {
  187. _backspace = NO;
  188. }
  189. - (void)textField:(__unused BTUIKFormField *)textField didInsertText:(__unused NSString *)text {
  190. // To be implemented by subclasses
  191. }
  192. #pragma mark - Custom accessors
  193. - (void)setText:( __unused NSString *)text {
  194. BOOL shouldChange = [self.textField.delegate textField:self.textField
  195. shouldChangeCharactersInRange:NSMakeRange(0, self.textField.text.length)
  196. replacementString:text];
  197. if (shouldChange) {
  198. [self.textField.editDelegate textField:self.textField willInsertText:text];
  199. self.textField.text = text;
  200. [self fieldContentDidChange];
  201. [self.textField.editDelegate textField:self.textField didInsertText:text];
  202. }
  203. [self updateAppearance];
  204. }
  205. - (NSString *)text {
  206. return self.textField.text;
  207. }
  208. #pragma mark - Delegate methods and handlers
  209. - (void)resetFormField {
  210. // To be implemented by subclass
  211. }
  212. - (BOOL)becomeFirstResponder {
  213. return [self.textField becomeFirstResponder];
  214. }
  215. - (void)fieldContentDidChange {
  216. // To be implemented by subclass
  217. if (self.delegate) {
  218. [self.delegate formFieldDidChange:self];
  219. }
  220. [self updateAppearance];
  221. }
  222. - (void)editingDidBegin {
  223. [self setAccessoryHighlighted:YES];
  224. }
  225. - (void)editingDidEnd {
  226. [self setAccessoryHighlighted:NO];
  227. }
  228. - (BOOL)textField:(__unused UITextField *)textField shouldChangeCharactersInRange:(__unused NSRange)range replacementString:(__unused NSString *)newText {
  229. // To be implemented by subclass
  230. return YES;
  231. }
  232. - (BOOL)textFieldShouldReturn:(__unused UITextField *)textField {
  233. if ([self.delegate respondsToSelector:@selector(formFieldShouldReturn:)]) {
  234. return [self.delegate formFieldShouldReturn:self];
  235. } else {
  236. return YES;
  237. }
  238. }
  239. - (void)tappedField {
  240. [self.textField becomeFirstResponder];
  241. }
  242. #pragma mark UIKeyInput
  243. - (void)insertText:(__unused NSString *)text {
  244. [self.textField insertText:text];
  245. }
  246. - (void)deleteBackward {
  247. [self.textField deleteBackward];
  248. }
  249. - (BOOL)hasText {
  250. return [self.textField hasText];
  251. }
  252. #pragma mark Accessibility Helpers
  253. - (NSString *)stripInvalidAccessibilityFromString:(NSString *)str {
  254. return [str stringByReplacingOccurrencesOfString:@"Invalid: " withString:@""];
  255. }
  256. - (NSString *)addInvalidAccessibilityToString:(NSString *)str {
  257. return [NSString stringWithFormat:@"Invalid: %@", [self stripInvalidAccessibilityFromString:str]];
  258. }
  259. #pragma mark Accessory View Helpers
  260. - (void)setAccessoryView:(UIView *)accessoryView {
  261. if (self.accessoryView && self.accessoryView.superview) {
  262. [self.accessoryView removeFromSuperview];
  263. _accessoryView = nil;
  264. }
  265. _accessoryView = accessoryView;
  266. self.accessoryView.translatesAutoresizingMaskIntoConstraints = NO;
  267. [self addSubview:self.accessoryView];
  268. [self.accessoryView setContentHuggingPriority:UILayoutPriorityRequired forAxis:UILayoutConstraintAxisHorizontal];
  269. [self.accessoryView setContentCompressionResistancePriority:UILayoutPriorityRequired forAxis:UILayoutConstraintAxisHorizontal];
  270. [self updateConstraints];
  271. }
  272. - (void)setAccessoryViewHidden:(BOOL)hidden animated:(__unused BOOL)animated {
  273. if (self.accessoryView == nil) {
  274. [self updateConstraints];
  275. return;
  276. }
  277. if (animated) {
  278. [UIView animateWithDuration:0.1 animations:^{
  279. self.accessoryView.hidden = hidden;
  280. [self updateConstraints];
  281. }];
  282. } else {
  283. self.accessoryView.hidden = hidden;
  284. [self updateConstraints];
  285. }
  286. }
  287. - (void)setAccessoryHighlighted:(BOOL)highlight {
  288. if (self.accessoryView) {
  289. if ([self.accessoryView respondsToSelector:@selector(setHighlighted:animated:)]) {
  290. SEL selector = @selector(setHighlighted:animated:);
  291. BOOL animated = YES;
  292. NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:[self.accessoryView methodSignatureForSelector:selector]];
  293. [invocation setSelector:selector];
  294. [invocation setTarget:self.accessoryView];
  295. [invocation setArgument:&highlight atIndex:2];
  296. [invocation setArgument:&animated atIndex:3];
  297. [invocation invoke];
  298. }
  299. }
  300. }
  301. @end