Delete Slide-to-type by word in UITextView gives incorrect range in `shouldChangeText` delegate method

492 Views Asked by At

I have a UITextView in a ViewController. I've implemented shouldChangeTextInRange delegate method from UITextViewDelegate. The range value I obtain through the above delegate method for deleting a single character is something like {location, 1}.

But if I use the slide-to-type mechanism to insert a word before performing delete operation, the entire word gets deleted instead of a single character. I need to keep track of this.

For example, I insert "Hello" using the slide-to-type mechanism. If I press delete key in the keyboard, I get {4, 1} as range but the entire word "Hello" has been deleted in text view. In this case, I need the range to be {0, 5} much like selecting the text "Hello" and deleting it.

Is there a way to differentiate between normal delete operation and slide-to-type delete operation? How can I get the actual range that has been deleted in the text view?

A little help is very much appreciated.

This is what I've tried.

class ViewController: UIViewController {

    override func viewDidLoad() {
        super.viewDidLoad()
        self.addTextView()
    }

    override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
        self.view.endEditing(true)
    }
}

extension ViewController {
    func addTextView() {
        let textContainer = NSTextContainer(size: CGSize(width: 200, height: CGFloat.greatestFiniteMagnitude))

        let layoutManager = NSLayoutManager()
        layoutManager.addTextContainer(textContainer)

        let textStorage = NSTextStorage()
        textStorage.delegate = self
        textStorage.addLayoutManager(layoutManager)

        let textViewFrame = CGRect(x: 50, y: 50, width: 200, height: 200)
        let textView = UITextView(frame: textViewFrame, textContainer: textContainer)
        textView.backgroundColor = .green
        textView.delegate = self

        self.view.addSubview(textView)
    }
}

extension ViewController: UITextViewDelegate {
    func textViewDidBeginEditing(_ textView: UITextView) {
        print("text editing began")
    }

    func textViewDidChange(_ textView: UITextView) {
        print("text view content changed")
    }

    func textViewDidChangeSelection(_ textView: UITextView) {
        print("text view selection changed")
    }

    func textView(_ textView: UITextView, shouldChangeTextIn range: NSRange, replacementText text: String) -> Bool {
        print("should change \(text) in \(range)")
        return true
    }

    func textViewDidEndEditing(_ textView: UITextView) {
        print("text editing ended")
    }
}

extension ViewController: NSTextStorageDelegate {
    func textStorage(_ textStorage: NSTextStorage, willProcessEditing editedMask: NSTextStorage.EditActions, range editedRange: NSRange, changeInLength delta: Int) {
        print("will process \(editedRange)")
    }

    func textStorage(_ textStorage: NSTextStorage, didProcessEditing editedMask: NSTextStorage.EditActions, range editedRange: NSRange, changeInLength delta: Int) {
        print("did process \(editedRange)")
    }
}

EDIT 1:

I have a separate storage mechanism on which I update the changes made to the text view based on the range and replacement text provided in shouldChangeText delegate method. I haven't included that for the sake of simplicity.

1

There are 1 best solutions below

1
On BEST ANSWER

I just noticed the exact same thing and struggled for quite a while trying to figure this out in an elegant way. It seems to me that this has to be a bug, so hopefully it will be fixed soon. I'll file a bug report with Apple after I submit the solution I came up with.

First, we need a way to keep track of when the user might have deleted an entire word (i.e., anytime they deleted anything we have to check in case they deleted more than just one character). We can do that in the UITextViewDelegate method textView(_:shouldChangeTextIn:replacementText:). We know that something was deleted when the replacement text is empty (i.e., text.count == 0).

// Variable to keep track of when a word might have been deleted
fileprivate var didJustDelete = false


func textView(_ textView: UITextView, shouldChangeTextIn range: NSRange, replacementText text: String) -> Bool {
  if text.count > 0 {
    // Code to handle adding text
    ...
  } else {
    // Code to handle deleting text
    didJustDelete = true
    ...
  }
}

Then, in textViewDidChange (another UITextViewDelegate method) we can determine the difference and handle it accordingly.

func textViewDidChange(_ textView: UITextView) {
  // Check for deleting an entire word
  if didJustDelete {
    didJustDelete = false

    let difference = actualText.count - textView.text.count
    if difference > 0 {
      // Code to handle deleting a word
      let range = NSRange(location: textView.text.count, length: difference)
      ...
    }
  }
}