杨锴
2025-04-16 09a372bc45fde16fd42257ab6f78b8deeecf720b
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
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
/**
 * Tencent is pleased to support the open source community by making QMUI_iOS available.
 * Copyright (C) 2016-2021 THL A29 Limited, a Tencent company. All rights reserved.
 * Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at
 * http://opensource.org/licenses/MIT
 * 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.
 */
 
//
//  QMUITextField.m
//  qmui
//
//  Created by QMUI Team on 16-11-03
//
 
#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 didInitialize];
        if (QMUICMIActivated) {
            UIColor *textColor = TextFieldTextColor;
            if (textColor) {
                self.textColor = textColor;
            }
            
            self.tintColor = TextFieldTintColor;
        }
    }
    return self;
}
 
- (instancetype)initWithCoder:(NSCoder *)aDecoder {
    if (self = [super initWithCoder:aDecoder]) {
        [self didInitialize];
    }
    return self;
}
 
- (void)didInitialize {
    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.shouldResponseToProgrammaticallyTextChanges = YES;
    self.maximumTextLength = NSUIntegerMax;
    
    if (QMUICMIActivated) {
        self.placeholderColor = UIColorPlaceholder;
        self.textInsets = TextFieldTextInsets;
    }
}
 
- (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}];
}
 
- (void)layoutSubviews {
    [super layoutSubviews];
    
    // 以下代码修复系统的 UITextField 在 iOS 10 下的 bug:https://github.com/Tencent/QMUI_iOS/issues/64
    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;
}
 
#pragma mark - Positioning Overrides
 
- (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];
}
 
- (CGRect)clearButtonRectForBounds:(CGRect)bounds {
    CGRect result = [super clearButtonRectForBounds:bounds];
    result = CGRectOffset(result, self.clearButtonPositionAdjustment.horizontal, self.clearButtonPositionAdjustment.vertical);
    return result;
}
 
#pragma mark - <UIResponderStandardEditActions>
 
- (BOOL)canPerformAction:(SEL)action withSender:(id)sender {
    BOOL superReturnValue = [super canPerformAction:action withSender:sender];
    if (action == @selector(paste:) && self.canPerformPasteActionBlock) {
        return self.canPerformPasteActionBlock(sender, superReturnValue);
    }
    return superReturnValue;
}
 
- (void)paste:(id)sender {
    BOOL shouldCallSuper = YES;
    if (self.pasteBlock) {
        shouldCallSuper = self.pasteBlock(sender);
    }
    if (shouldCallSuper) {
        [super paste:sender];
    }
}
 
 
@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 那里限制。
        if (textField.markedTextRange) {
            if ([textField.delegate respondsToSelector:@selector(textField:shouldChangeCharactersInRange:replacementString:originalValue:)]) {
                return [textField.delegate textField:textField shouldChangeCharactersInRange:range replacementString:string originalValue:YES];
            }
            return YES;
        }
        
        if (NSMaxRange(range) > textField.text.length) {
            // 如果 range 越界了,继续返回 YES 会造成 crash
            // https://github.com/Tencent/QMUI_iOS/issues/377
            // https://github.com/Tencent/QMUI_iOS/issues/1170
            // 这里的做法是本次返回 NO,并将越界的 range 缩减到没有越界的范围,再手动做该范围的替换。
            range = NSMakeRange(range.location, range.length - (NSMaxRange(range) - textField.text.length));
            if (range.length > 0) {
                UITextRange *textRange = [self.textField qmui_convertUITextRangeFromNSRange:range];
                [self.textField replaceRange:textRange withText:string];
            }
            return NO;
        }
        
        if (!string.length && range.length > 0) {
            // 允许删除,这段必须放在上面 #377、#1170 的逻辑后面
            if ([textField.delegate respondsToSelector:@selector(textField:shouldChangeCharactersInRange:replacementString:originalValue:)]) {
                return [textField.delegate textField:textField shouldChangeCharactersInRange:range replacementString:string originalValue:YES];
            }
            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) {
                    BOOL shouldChange = YES;
                    if ([textField.delegate respondsToSelector:@selector(textField:shouldChangeCharactersInRange:replacementString:originalValue:)]) {
                        shouldChange = [textField.delegate textField:textField shouldChangeCharactersInRange:range replacementString:allowedText originalValue:YES];
                    }
                    if (!shouldChange) {
                        return NO;
                    }
                    textField.text = [textField.text stringByReplacingCharactersInRange:range withString:allowedText];
                    // 通过代码 setText: 修改的文字,默认光标位置会在插入的文字开头,通常这不符合预期,因此这里将光标定位到插入的那段字符串的末尾
                    // 注意由于粘贴后系统也会在下一个 runloop 去修改光标位置,所以我们这里也要 dispatch 到下一个 runloop 才能生效,否则会被系统的覆盖
                    // https://github.com/Tencent/QMUI_iOS/issues/1282
                    dispatch_async(dispatch_get_main_queue(), ^{
                        textField.qmui_selectedRange = NSMakeRange(range.location + allowedText.length, 0);
                    });
                    
                    if (!textField.shouldResponseToProgrammaticallyTextChanges) {
                        [textField fireTextDidChangeEventForTextField:textField];
                    }
                }
            }
            
            if ([textField.delegate respondsToSelector:@selector(textField:didPreventTextChangeInRange:replacementString:)]) {
                [textField.delegate textField:textField didPreventTextChangeInRange:range replacementString:string];
            }
            return NO;
        }
    }
    
    if ([textField.delegate respondsToSelector:@selector(textField:shouldChangeCharactersInRange:replacementString:originalValue:)]) {
        return [textField.delegate textField:textField shouldChangeCharactersInRange:range replacementString:string originalValue:YES];
    }
    
    return YES;
}
 
- (void)handleTextChangeEvent:(QMUITextField *)textField {
    // 1、iOS 10 以下的版本,从中文输入法的候选词里选词输入,是不会走到 textField:shouldChangeCharactersInRange:replacementString: 的,所以要在这里截断文字
    // 2、如果是中文输入法正在输入拼音的过程中(markedTextRange 不为 nil),是不应该限制字数的(例如输入“huang”这5个字符,其实只是为了输入“黄”这一个字符),所以在 shouldChange 那边不会限制,而是放在 didChange 这里限制。
    
    // 系统的三指撤销在文本框达到最大字符长度限制时可能引发 crash
    // https://github.com/Tencent/QMUI_iOS/issues/1168
    if (textField.maximumTextLength < NSUIntegerMax && (textField.undoManager.undoing || textField.undoManager.redoing)) {
        return;
    }
    
    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/Tencent/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