Bootstrap

是时候封装限制 UITextField、UITextView 的最大字符长度了

在输入电话号码,设置昵称时,App 一般会设置一个允许输入的最大长度,来提示用户输入的内容过长了。

当用户输入的是纯英文字母、数字时,只需要在 UITextField、UITextView 的代理中判断用户输入的字符长度就可以了。但是在输入中文时,有一个从输入的拼音字符到联想到的汉字,再到确认汉字的过程。整个过程,UITextField 和 UITextView 都会通知代理用户输入的字符产生了变化。但是,在用户输入拼音字符时,也就是还没有确实汉字之前,就判断字符长度就会出现差错。

当用户输入中文时,如何知道 UITextField 或者 UITextView 处于输入了拼音,还处于汉字联想阶段呢?

可以通过输入框当前是否有高亮部分进行判断,如果有高亮部分,则意味着当前正在输入,这时就不进行字数统计;反之没有高亮部分时,就代表用户已经确认了一个汉字或者词语,这时候就可以做字符长度的判断了。

public protocol MaxLengthType: UITextInput {

    /// 需要判断输入最大长度的字符串
    ///
    /// UITextField 中 text 的定义为:
    /// open var text: String? // default is nil
    ///
    /// UITextView 中 text 的定义为:
    /// open var text: String!
    ///
    /// 所有通过 userInput 将两处不同的定义进行统一
    var userInput: String? { get set }
}

extension UITextView: MaxLengthType {
    public var userInput: String? {
        get {
            return self.text
        }
        set {
            self.text = newValue
        }
    }
}

extension UITextField: MaxLengthType {
    public var userInput: String? {
        get {
            return self.text
        }
        set {
            self.text = newValue
        }
    }
}

private var maxLengthKey: Void?
private var observerKey: Void?

extension MaxLengthType  {
    
    /// 文本限制的最大长度
    /// 为 nil 是表示不限制
    public var maxLength: Int? {
        get {
            return getAssociatedObject(self, &maxLengthKey)
        }
        set {
            setRetainedAssociatedObject(self, &maxLengthKey, newValue)
            if newValue != nil {
                if observer == nil {
                    observer = Observer(textInput: self,
                                        textDidChange: { [weak self] (notification) in
                                            self?.checkUserInputForMaxLength()
                                        })
                }
                checkUserInputForMaxLength()
            } else {
                observer = nil
            }
        }
    }
    
    func checkUserInputForMaxLength() {
        // 高亮部分为空时,并且 maxLength 存在时,再判断字符长度
        guard self.markedTextRange == nil,
              let maxLength = maxLength else {
            return
        }
        
        guard let text = self.userInput,
              text.count > maxLength else {
            return
        }
        let endIndex = text.index(text.startIndex, offsetBy: maxLength)
        userInput = String(text[text.startIndex.. Void
    
    init(textInput: UITextInput, textDidChange: @escaping (Notification) -> Void) {
        self.textDidChange = textDidChange
        if textInput is UITextField {
            NotificationCenter.default
                .addObserver(self, selector: #selector(textDidChange(notification:)),
                             name: .UITextFieldTextDidChange,
                             object: textInput)
        } else if textInput is UITextView {
            NotificationCenter.default
                .addObserver(self,
                             selector: #selector(textDidChange(notification:)),
                             name: .UITextViewTextDidChange,
                             object: textInput)
        }
    }
    
    deinit {
        NotificationCenter.default.removeObserver(self)
    }
    
    @objc func textDidChange(notification: Notification) {
        textDidChange(notification)
    }
}

func getAssociatedObject(_ object: Any, _ key: UnsafeRawPointer) -> T? {
    return objc_getAssociatedObject(object, key) as? T
}

func setRetainedAssociatedObject(_ object: Any, _ key: UnsafeRawPointer, _ value: T) {
    objc_setAssociatedObject(object, key, value, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
}

完整的项目地址为