34

can anybody help me with this: i need to implement UITextField for input number. This number should always be in decimal format with 4 places e.g. 12.3456 or 12.3400. So I created NSNumberFormatter that helps me with decimal places.

I am setting the UITextField value in

-(BOOL)textField:(UITextField *)textField shouldChangeCharactersInRange:(NSRange)range replacementString:(NSString *)string method. I proccess input, use formatter and finally call [textField setText: myFormattedValue];

This works fine but this call also moves the cursor to the end of my field. That is unwanted. E.g. I have 12.3400 in my field and the cursor is located on the very beginning and user types number 1. The result value is 112.3400 but cursor is moved at the end. I want to end with cursor when the user expects (just after the number 1 recently added). There are some topics about setting cursor in TextView but this is UITextField. I also tried to catch selectedTextRange of the field, which saves the cursor position properly but after setText method call, this automatically changes and the origin UITextRange is lost (changed to current). hopefully my explanation is clear.

Please, help me with this. thank you very much.

EDIT : Finally, i decided to switch the functionality to changing the format after whole editing and works good enough. I have done it by adding a selector forControlEvents:UIControlEventEditingDidEnd.

Sujay
  • 2,510
  • 2
  • 27
  • 47
user1135839
  • 563
  • 2
  • 7
  • 14
  • When you read the `selectedTextRange`, can you not copy it as a property in the class that acts as the `UITextFieldDelegate` and then re-set it after `setText:`? – Phillip Mills Jun 22 '12 at 14:12
  • 1
    Phillip, that doesn't work. After you set the text with setText, the UITextRange you collected before setting the text is reset to null, and so you can't reapply it afterwards to the finished UITextField. – JasonD Feb 09 '13 at 23:10

6 Answers6

41

After the UITextField.text property is changed, any previous references to UITextPosition or UITextRange objects that were associated with the old text will be set to nil after you set the text property. You need to store what the text offset will be after the manipulation will be BEFORE you set the text property.

This worked for me (note, you do have to test whether cursorOffset is < textField.text.length if you remove any characters from t in the example below):

- (BOOL) textField:(UITextField *) textField shouldChangeCharactersInRange:(NSRange) range replacementString:(NSString *) string

{
    UITextPosition *beginning = textField.beginningOfDocument;
    UITextPosition *start = [textField positionFromPosition:beginning offset:range.location];
    UITextPosition *end = [textField positionFromPosition:start offset:range.length];
    UITextRange *textRange = [textField textRangeFromPosition:start toPosition:end];

    // this will be the new cursor location after insert/paste/typing
    NSInteger cursorOffset = [textField offsetFromPosition:beginning toPosition:start] + string.length; 

    // now apply the text changes that were typed or pasted in to the text field
    [textField replaceRange:textRange withText:string];

    // now go modify the text in interesting ways doing our post processing of what was typed...
    NSMutableString *t = [textField.text mutableCopy];
    t = [t upperCaseString];
    // ... etc

    // now update the text field and reposition the cursor afterwards
    textField.text = t;
    UITextPosition *newCursorPosition = [textField positionFromPosition:textField.beginningOfDocument offset:cursorOffset];
    UITextRange *newSelectedRange = [textField textRangeFromPosition:newCursorPosition toPosition:newCursorPosition];
    [textField setSelectedTextRange:newSelectedRange];

    return NO;
}
Rui Peres
  • 25,741
  • 9
  • 87
  • 137
JasonD
  • 7,472
  • 5
  • 31
  • 31
5

And here is the swift version :

func textField(textField: UITextField, shouldChangeCharactersInRange range: NSRange, replacementString string: String) -> Bool {
    let beginning = textField.beginningOfDocument
    let start = textField.positionFromPosition(beginning, offset:range.location)
    let end = textField.positionFromPosition(start!, offset:range.length)
    let textRange = textField.textRangeFromPosition(start!, toPosition:end!)
    let cursorOffset = textField.offsetFromPosition(beginning, toPosition:start!) + string.characters.count

// just used same text, use whatever you want :)
    textField.text = (textField.text! as NSString).stringByReplacingCharactersInRange(range, withString: string)

    let newCursorPosition = textField.positionFromPosition(textField.beginningOfDocument, offset:cursorOffset)
    let newSelectedRange = textField.textRangeFromPosition(newCursorPosition!, toPosition:newCursorPosition!)
    textField.selectedTextRange = newSelectedRange

    return false
}
ergunkocak
  • 3,334
  • 1
  • 32
  • 31
  • Im using xamarin.ios and had to translate the method to C#. The execution time of the whole method is painfully slow, which makes fast writing unenjoyable. As a non-xamarin-user are u experiencing bad performance as well? – Csharpest Aug 01 '18 at 14:30
4

Here is a swift 3 version

extension UITextField {
    func setCursor(position: Int) {
        let position = self.position(from: beginningOfDocument, offset: position)!
        selectedTextRange = textRange(from: position, to: position)
    }
}
Jeremy Piednoel
  • 407
  • 2
  • 5
  • 12
4

What actually worked for me was very simple, just used dispatch main async:

DispatchQueue.main.async(execute: {
  textField.selectedTextRange = textField.textRange(from: start, to: end)
})
Everton Cunha
  • 1,017
  • 8
  • 10
  • This work for me. I have to to add under `public func textView( _ textView: UITextView, shouldChangeTextIn range: NSRange, replacementText text: String ) -> Bool` – tylerlantern Mar 23 '20 at 07:59
  • very strange, but it works! it seems that moving cursor should happen and happens on the next run loop cycle – Mr.Fingers Sep 10 '20 at 11:23
2

Implement the code described in this answer: Moving the cursor to the beginning of UITextField

NSRange beginningRange = NSMakeRange(0, 0);
NSRange currentRange = [textField selectedRange];
if(!NSEqualRanges(beginningRange, currentRange))
{
    [textField setSelectedRange:beginningRange];
}

EDIT: From this answer, it looks like you can just use this code with your UITextField if you're using iOS 5 or above. Otherwise, you need to use a UITextView instead.

Community
  • 1
  • 1
gtmtg
  • 3,010
  • 2
  • 22
  • 35
0

Swift X. To prevent from moving cursor from last position.

func textFieldDidChangeSelection(_ textField: UITextField) {
    let point = CGPoint(x: bounds.maxX, y: bounds.height / 2)
    if let textPosition = textField.closestPosition(to: point) {
        textField.selectedTextRange = textField.textRange(from: textPosition, to: textPosition)
    }
}
Mike Glukhov
  • 1,758
  • 19
  • 18