Search code examples
iosswifttextviewdelegatesuikit

UITextView delegate textViewDidChangeSelection is called twice


I'm trying to do some stuff when user taps Enter button, so I have implemented the following delegates for UITextView:

// Delegate is called when text is gonna change
func textView(_ textView: UITextView, shouldChangeTextIn range: NSRange, replacementText text: String) -> Bool {
    if (text == "\n") {
        // Some behavior when user taps Enter
        return true
    }

    // Some other code
    return true
}

// Delegate is called when selection is changed
func textViewDidChangeSelection(_ textView: UITextView) {
    // Some code
}

The problem is, textViewDidChangeSelection is called twice when user taps Enter.

In second call, textView.selectedRange is changed to the last character of text view. This produces a problem when user taps Enter after any line in the middle of text, because caret position is changed to end of text.

I've attached a ‏reproducible example here so you can check, I'm not sure if the problem is in the delegates, or in the way I attached the delegate to the view.

To re-produce the problem, do the following scenario:

  1. Write some lines, for example:

    Line 1.

    Bla bla bla

    line 3

  2. Go to the end of line 1.

  3. Tap on Enter.

The new line is there in the right position, but the caret position is changed to the end of text view.

Notes:

  • I've checked the following post in stackoverflow, but it doesn't fix my problem.
  • The file you need to review in my ‏reproducible example is EditorTextView.swift only.

Solution

  • OK finally, I found a solution for this weird behavior of delegation in the UITextView wrapper for SwiftUI.

    So the entire issue was with setting the text again every time updateUIView was called. That is not necessary and it caused a weird feedback loop with textViewDidChange delegate method (that is where I set the selected range when the last character was \n, which is the case for a new line).

    I don't know exactly why does that happen, I just think it's a bug with how UIViewRepresentables works under the hood.

    So, to solve the issue, I had to move the following line from updateUIView:

    uiView.text = document
    

    And add it to makeUIView:

    textView.text = document
    

    This way, it will be bound permanently.