forked from Tencent/QMUI_iOS
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathQMUITextField.m
216 lines (169 loc) · 8.64 KB
/
QMUITextField.m
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
//
// QMUITextField.m
// qmui
//
// Created by MoLice on 16-11-03
// Copyright (c) 2016年 QMUI Team. All rights reserved.
//
#import "QMUITextField.h"
#import "QMUICore.h"
#import "NSString+QMUI.h"
#import "UITextField+QMUI.h"
#import "QMUIMultipleDelegates.h"
// 私有的类,专用于实现 QMUITextFieldDelegate,避免 self.delegate = self 的写法(以前是 QMUITextField 自己实现了 delegate)
@interface _QMUITextFieldDelegator : NSObject <QMUITextFieldDelegate, UIScrollViewDelegate>
@property(nonatomic, weak) QMUITextField *textField;
- (void)handleTextChangeEvent:(QMUITextField *)textField;
@end
@interface QMUITextField ()
@property(nonatomic, strong) _QMUITextFieldDelegator *delegator;
@end
@implementation QMUITextField
@dynamic delegate;
- (instancetype)initWithFrame:(CGRect)frame {
self = [super initWithFrame:frame];
if (self) {
[self didInitialized];
self.tintColor = TextFieldTintColor;
}
return self;
}
- (instancetype)initWithCoder:(NSCoder *)aDecoder {
if (self = [super initWithCoder:aDecoder]) {
[self didInitialized];
}
return self;
}
- (void)didInitialized {
self.qmui_multipleDelegatesEnabled = YES;
self.delegator = [[_QMUITextFieldDelegator alloc] init];
self.delegator.textField = self;
self.delegate = self.delegator;
[self addTarget:self.delegator action:@selector(handleTextChangeEvent:) forControlEvents:UIControlEventEditingChanged];
self.placeholderColor = UIColorPlaceholder;
self.textInsets = TextFieldTextInsets;
self.shouldResponseToProgrammaticallyTextChanges = YES;
self.maximumTextLength = NSUIntegerMax;
}
- (void)dealloc {
self.delegate = nil;
}
#pragma mark - Placeholder
- (void)setPlaceholderColor:(UIColor *)placeholderColor {
_placeholderColor = placeholderColor;
if (self.placeholder) {
[self updateAttributedPlaceholderIfNeeded];
}
}
- (void)setPlaceholder:(NSString *)placeholder {
[super setPlaceholder:placeholder];
if (self.placeholderColor) {
[self updateAttributedPlaceholderIfNeeded];
}
}
- (void)updateAttributedPlaceholderIfNeeded {
self.attributedPlaceholder = [[NSAttributedString alloc] initWithString:self.placeholder attributes:@{NSForegroundColorAttributeName: self.placeholderColor}];
}
#pragma mark - TextInsets
- (CGRect)textRectForBounds:(CGRect)bounds {
bounds = CGRectInsetEdges(bounds, self.textInsets);
CGRect resultRect = [super textRectForBounds:bounds];
return resultRect;
}
- (CGRect)editingRectForBounds:(CGRect)bounds {
bounds = CGRectInsetEdges(bounds, self.textInsets);
return [super editingRectForBounds:bounds];
}
#pragma mark - TextPosition
- (void)layoutSubviews {
[super layoutSubviews];
// 以下代码修复系统的 UITextField 在 iOS 10 下的 bug:https://github.com/QMUI/QMUI_iOS/issues/64
if (@available(iOS 10.0, *)) {
UIScrollView *scrollView = self.subviews.firstObject;
if (![scrollView isKindOfClass:[UIScrollView class]]) {
return;
}
// 默认 delegate 是为 nil 的,所以我们才利用 delegate 修复这 个 bug,如果哪一天 delegate 不为 nil,就先不处理了。
if (scrollView.delegate) {
return;
}
scrollView.delegate = self.delegator;
}
}
- (void)setText:(NSString *)text {
NSString *textBeforeChange = self.text;
[super setText:text];
if (self.shouldResponseToProgrammaticallyTextChanges && ![textBeforeChange isEqualToString:text]) {
[self fireTextDidChangeEventForTextField:self];
}
}
- (void)setAttributedText:(NSAttributedString *)attributedText {
NSAttributedString *textBeforeChange = self.attributedText;
[super setAttributedText:attributedText];
if (self.shouldResponseToProgrammaticallyTextChanges && ![textBeforeChange isEqualToAttributedString:attributedText]) {
[self fireTextDidChangeEventForTextField:self];
}
}
- (void)fireTextDidChangeEventForTextField:(QMUITextField *)textField {
[textField sendActionsForControlEvents:UIControlEventEditingChanged];
[[NSNotificationCenter defaultCenter] postNotificationName:UITextFieldTextDidChangeNotification object:textField];
}
- (NSUInteger)lengthWithString:(NSString *)string {
return self.shouldCountingNonASCIICharacterAsTwo ? string.qmui_lengthWhenCountingNonASCIICharacterAsTwo : string.length;
}
@end
@implementation _QMUITextFieldDelegator
#pragma mark - <QMUITextFieldDelegate>
- (BOOL)textField:(QMUITextField *)textField shouldChangeCharactersInRange:(NSRange)range replacementString:(NSString *)string {
if (textField.maximumTextLength < NSUIntegerMax) {
// 如果是中文输入法正在输入拼音的过程中(markedTextRange 不为 nil),是不应该限制字数的(例如输入“huang”这5个字符,其实只是为了输入“黄”这一个字符),所以在 shouldChange 这里不会限制,而是放在 didChange 那里限制。
BOOL isDeleting = range.length > 0 && string.length <= 0;
if (isDeleting || textField.markedTextRange) {
return YES;
}
NSUInteger rangeLength = textField.shouldCountingNonASCIICharacterAsTwo ? [textField.text substringWithRange:range].qmui_lengthWhenCountingNonASCIICharacterAsTwo : range.length;
if ([textField lengthWithString:textField.text] - rangeLength + [textField lengthWithString:string] > textField.maximumTextLength) {
// 将要插入的文字裁剪成这么长,就可以让它插入了
NSInteger substringLength = textField.maximumTextLength - [textField lengthWithString:textField.text] + rangeLength;
if (substringLength > 0 && [textField lengthWithString:string] > substringLength) {
NSString *allowedText = [string qmui_substringAvoidBreakingUpCharacterSequencesWithRange:NSMakeRange(0, substringLength) lessValue:YES countingNonASCIICharacterAsTwo:textField.shouldCountingNonASCIICharacterAsTwo];
if ([textField lengthWithString:allowedText] <= substringLength) {
textField.text = [textField.text stringByReplacingCharactersInRange:range withString:allowedText];
if (!textField.shouldResponseToProgrammaticallyTextChanges) {
[textField fireTextDidChangeEventForTextField:textField];
}
}
}
if ([textField.delegate respondsToSelector:@selector(textField:didPreventTextChangeInRange:replacementString:)]) {
[textField.delegate textField:textField didPreventTextChangeInRange:range replacementString:string];
}
return NO;
}
}
return YES;
}
- (void)handleTextChangeEvent:(QMUITextField *)textField {
// 1、iOS 10 以下的版本,从中文输入法的候选词里选词输入,是不会走到 textField:shouldChangeCharactersInRange:replacementString: 的,所以要在这里截断文字
// 2、如果是中文输入法正在输入拼音的过程中(markedTextRange 不为 nil),是不应该限制字数的(例如输入“huang”这5个字符,其实只是为了输入“黄”这一个字符),所以在 shouldChange 那边不会限制,而是放在 didChange 这里限制。
if (!textField.markedTextRange) {
if ([textField lengthWithString:textField.text] > textField.maximumTextLength) {
textField.text = [textField.text qmui_substringAvoidBreakingUpCharacterSequencesWithRange:NSMakeRange(0, textField.maximumTextLength) lessValue:YES countingNonASCIICharacterAsTwo:textField.shouldCountingNonASCIICharacterAsTwo];
if ([textField.delegate respondsToSelector:@selector(textField:didPreventTextChangeInRange:replacementString:)]) {
[textField.delegate textField:textField didPreventTextChangeInRange:textField.qmui_selectedRange replacementString:nil];
}
}
}
}
#pragma mark - <UIScrollViewDelegate>
- (void)scrollViewDidScroll:(UIScrollView *)scrollView {
// 以下代码修复系统的 UITextField 在 iOS 10 下的 bug:https://github.com/QMUI/QMUI_iOS/issues/64
if (scrollView != self.textField.subviews.firstObject) {
return;
}
CGFloat lineHeight = ((NSParagraphStyle *)self.textField.defaultTextAttributes[NSParagraphStyleAttributeName]).minimumLineHeight;
lineHeight = lineHeight ?: ((UIFont *)self.textField.defaultTextAttributes[NSFontAttributeName]).lineHeight;
if (scrollView.contentSize.height > ceil(lineHeight) && scrollView.contentOffset.y < 0) {
scrollView.contentOffset = CGPointMake(scrollView.contentOffset.x, 0);
}
}
@end