13

The predictive-input of iOS8 calls the following delegate method of UITextView multiple times resulting in the selected word being inserted multiple times into the view.

This code works for typing single letters and copy/paste but not when using the predictive-input bar; why not?

- (BOOL) textView:(UITextView*)textView shouldChangeTextInRange:(NSRange)range replacementText:(NSString*)text
{
    textView.text = [textView.text stringByReplacingCharactersInRange:range withString:text];
    return false;
}

With this code; if I enter an empty UITextView and tap on "The" in the predictive text (autocomplete) view it inserts "The The" into the view by way of making three calls on this method. The parameters passed in for each call are:

  • range : {0,0} text : @"The"
  • range : {0,0} text : @"The"
  • range : {3,0} text : @" "

The space I can understand; but why insert "The" twice?

Wex
  • 4,434
  • 3
  • 33
  • 47

3 Answers3

20

I got this same issue. It appears that with predictive text, setting textView.text in that delegate method triggers an immediate call to that delegate method again (this only happens with predictive text as far as I know).

I fixed it by just surrounding my textView changes with a guard:

private var hack_shouldIgnorePredictiveInput = false

func textView(textView: UITextView!, shouldChangeTextInRange range: NSRange, replacementText text: String!) -> Bool {
    if hack_shouldIgnorePredictiveInput {
        hack_shouldIgnorePredictiveInput = false
        return false
    }

    hack_shouldIgnorePredictiveInput = true

    textView.text = "" // Modify text however you need. This will cause shouldChangeTextInRange to be called again, but it will be ignored thanks to hack_shouldIgnorePredictiveInput

    hack_shouldIgnorePredictiveInput = false

    return false
}
Richard Venable
  • 8,310
  • 3
  • 49
  • 52
  • I discovered that too and did the exactly the same thing in the end. – Wex Nov 07 '14 at 15:15
  • The 3rd call providing the @" " is done post return of the first shouldChangeTextInRange: So the if hack_shouldIgnorePredictiveInput can simply return false, and the blocking flag will go back to false before the empty space is submitted. – InitJason Oct 26 '16 at 20:39
1

I modified the accepted answer from Richard Venable, because, as JLust noted in the comment, that 3rd call with the space was throwing me off.

I added

private var predictiveTextWatcher = 0

And

if predictiveTextWatcher == 1 {
            predictiveTextWatcher = 0
            return false
        }

        if hack_shouldIgnorePredictiveInput {
            predictiveTextWatcher += 1
            hack_shouldIgnorePredictiveInput = false
            return false
        }

It's all pretty hacky, but better than nothing.

Best,

MScottWaller
  • 3,321
  • 2
  • 24
  • 47
1

Not an answer, but a safer workaround:

class TextViewTextChangeChecker {
    private var timestamp: TimeInterval = 0
    private var lastRange: NSRange = NSRange(location: -1, length: 0)
    private var lastText: String = ""

    func shouldChange(text:String,in range: NSRange) -> Bool {
        let SOME_SHORT_TIME = 0.1
        let newStamp = Date().timeIntervalSince1970
        let same = lastText == text && range == lastRange && newStamp - timestamp < SOME_SHORT_TIME
        timestamp = newStamp
        lastRange = range
        lastText = text
        return !same
    }
}

still this didn't helped me because changing the textView from the shouldChangeTextInRange function changed the autocapitalizationType to .word (only by behaviour, not the field itself).

Yedidya Reiss
  • 5,316
  • 2
  • 17
  • 19