Format UITextField text without having cursor move

2020-05-14 18:34发布

I am trying to apply NSAttributedString styles to a UITextField after processing a new text entry, keystroke by keystroke. The problem is that any time I replace the text the cursor will jump the very end on each change. Here's my current approach …

Receive and display text change immediately (same issue applies when I return false and do the text replacement manually)

func textField(textField: UITextField!, shouldChangeCharactersInRange range: NSRange, replacementString string: String!) -> Bool {        
    let range:NSRange = NSRange(location: range.location, length: range.length)
    let newString = (textField.text as NSString).stringByReplacingCharactersInRange(range, withString: string);
    return true
}

I subscribed to the UITextFieldTextDidChangeNotification notification. This triggers the styling. When the text is changed I replace it (NSString) with the formatted version (NSAttributedString) using some format() function.

func textFieldTextDidChangeNotification(userInfo:NSDictionary) {
    var attributedString:NSMutableAttributedString = NSMutableAttributedString(string: fullString)
    attributedString = format(textField.text) as NSMutableAttributedString
    textField.attributedText = attributedString
}

Styling works fine like this. However after each text replacement the cursor jumps to the end of the string. How can I turn off this behaviour or manually move the cursor back where it started the edit? … or is there a better solution to style the text while editing?

6条回答
够拽才男人
2楼-- · 2020-05-14 19:15

This is a code working for swift 2. Enjoy it!

 func textField(textField: UITextField, shouldChangeCharactersInRange range: NSRange, replacementString string: String) -> Bool {
    let beginnig: UITextPosition =  textField.beginningOfDocument

    if let txt = textField.text {
        textField.text = NSString(string: txt).stringByReplacingCharactersInRange(range, withString: string)
    }

    if let cursorLocation: UITextPosition = textField.positionFromPosition(beginnig, offset: (range.location + string.characters.count) ) {
        textField.selectedTextRange = textField.textRangeFromPosition(cursorLocation, toPosition: cursorLocation)
    }

    return false
}

Note that last "if let" should always stay at the end of code.

查看更多
Melony?
3楼-- · 2020-05-14 19:24

To piggy back off an answer to a different question here: https://stackoverflow.com/a/51814368/431271, you shouldn't be modifying the text in shouldChangeCharactersInRange since that delegate method is intended only to let the field know whether or not to allow a change and isn't supposed to mutate.

Instead, I like to handle the text change by subscribing to the value change, like this:

textField.addTarget(self, action: #selector(handleTextChanged), for: .editingChanged)

// elsewhere in the file
func handleTextChanged(_ textField: UITextField) {
    textField.sanitizeText(map: formatText)
}

where the implementation of sanitizeText looks like this:

extension UITextField {

    // Use this to filter out or change the text value without 
    // losing the current selection
    func sanitizeText(map: ((String) -> String)) {
        guard let text = self.text,
            let selection = selectedTextRange else {
                return
        }

        let newText = map(text)

        // only execute below if text is different
        guard newText != text else { return }

        // determine where new cursor position should start
        // so the cursor doesnt get sent to the end
        let diff = text.count - newText.count
        let cursorPosition = offset(from: beginningOfDocument, to: selection.start) - diff

        self.text = newText

        // notify the value changed (to ensure delegate methods get triggered)
        sendActions(for: .valueChanged)

        // update selection afterwards
        if let newPosition = position(from: beginningOfDocument, offset: cursorPosition) {
            selectedTextRange = textRange(from: newPosition, to: newPosition)
        }
    }
}
查看更多
【Aperson】
4楼-- · 2020-05-14 19:28

Thanks @Stonz2 by the code in Objective-C. It works like a charm! I used it in my Swift project. The same code in Swift:

func textField(textField: UITextField, shouldChangeCharactersInRange range: NSRange, replacementString string: String) -> Bool {

    let positionOriginal = textField.beginningOfDocument
    let cursorLocation = textField.position(from: positionOriginal, offset: (range.location + NSString(string: string).length))

    /* MAKE YOUR CHANGES TO THE FIELD CONTENTS AS NEEDED HERE */

    if let cursorLoc = cursorLocation {
        textField.selectedTextRange = textField.textRange(from: cursorLoc, to: cursorLoc)
    }
    return false 
}
查看更多
Anthone
5楼-- · 2020-05-14 19:30

The way to make this work is to grab the location of the cursor, update the field contents, and then replace the cursor to its original position. I'm not sure of the exact equivalent in Swift, but the following is how I would do it in Obj-C.

- (BOOL)textField:(UITextField *)textField shouldChangeCharactersInRange:(NSRange)range replacementString:(NSString *)string
{
    UITextPosition *beginning = textField.beginningOfDocument;
    UITextPosition *cursorLocation = [textField positionFromPosition:beginning offset:(range.location + string.length)];

    textField.text = [textField.text stringByReplacingCharactersInRange:range withString:string];

    /* MAKE YOUR CHANGES TO THE FIELD CONTENTS AS NEEDED HERE */

    // cursorLocation will be (null) if you're inputting text at the end of the string
    // if already at the end, no need to change location as it will default to end anyway
    if(cursorLocation)
    {
        // set start/end location to same spot so that nothing is highlighted
        [textField setSelectedTextRange:[textField textRangeFromPosition:cursorLocation toPosition:cursorLocation]];
    }

    return NO;
}
查看更多
仙女界的扛把子
6楼-- · 2020-05-14 19:32

To complement the other correct answers in this thread here are some code snippets for Swift 4.2 and for both UITextField and UITextView

UITextField

func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
    // Update Cursor
    let positionOriginal = textField.beginningOfDocument
    let cursorLocation = textField.position(from: positionOriginal, offset: (range.location + string.count))
    if let cursorLocation = cursorLocation {
        textField.selectedTextRange = textField.textRange(from: cursorLocation, to: cursorLocation)
    }
    return false
}

UITextView

func textView(_ textView: UITextView, shouldChangeTextIn range: NSRange, replacementText text: String) -> Bool {
    // Update Cursor
    let positionOriginal = textView.beginningOfDocument
    let cursorLocation = textView.position(from: positionOriginal, offset: (range.location + text.count))
    if let cursorLocation = cursorLocation {
        textView.selectedTextRange = textView.textRange(from: cursorLocation, to: cursorLocation)
    }
    return false
}
查看更多
放我归山
7楼-- · 2020-05-14 19:38

This is an old question, but I had a similar issue and resolved it with the following Swift:

// store the current cursor position as a range
var preAttributedRange: NSRange = textField.selectedRange 
// apply attributed string
var attributedString:NSMutableAttributedString = NSMutableAttributedString(string: fullString)
attributedString = format(textField.text) as NSMutableAttributedString
textField.attributedText = attributedString
// reapply the range
textField.selectedRange = preAttributedRange

It works in the context of my app, hopefully it's useful for someone!

查看更多
登录 后发表回答