Understanding UITextView Input

Hi everyone,
I have been playing around with the shouldChangeTextIn method, trying to create some code that will exclude certain characters from the textView, and noticed that the first character typed into the textView doesn't register in the console output. If I type 'a' it returns that is it not a letter on the first input, but does acknowledge it as a letter from then on. Likewise, if I switch to numbers, the initial input isn't acknowledged as not being a letter. I'm guessing that the initial input doesn't actually register until the second character is typed in, so everything is a step out from then on. Can anyone point me to some documentation that will enlighten me on this? Should the first output just be ignored, tossed to the trash so to speak, while everything from then on is treated as having a value?

func textView(_ textView: UITextView, shouldChangeTextIn range: NSRange, replacementText text: String) -> Bool {


let currentText = textView.text ?? ""

let singleLetter = currentText.last ?? " "

if singleLetter.isLetter == true {

print("Is letter")

return true

}else {

print("Is not letter")

return true

}

}

P.S. Apologies if this is posted in the wrong section. I think I might have it right, but am not entirely sure.

Replies

This function is called BEFORE changing text property in TextView. Means using textView.text will not give you the text AFTER changes.

To get value AFTER changes you should do something like:


let resultingText = (textView.text as NSString).replacingCharacters(in: range, with: text)


and keep in mind, it is not good to check last letter only, because user can Paste several characters at once (unless you disable this ability)

Hi,


textView:shouldChangeTextIn:replacementText is called before any text in the UITextView is changed. it gives you a chance to accept or reject the edit that will be made to the UITextView.text before any change is made (the edit is specified both by the range in the text that will be replaced and by the replacementText). if you return true, the text will be updated.


you should be asking about what the replacementText is -- that's any new text that was typed or pasted, and it may not be just one character -- not what the UITextView.text is (no edit has not yet been made). so something like this should work to get you started:


func textView(_ textView: UITextView, shouldChangeTextIn range: NSRange, replacementText text: String) -> Bool {

    let singleLetter = replacementText.last ?? " "
   
    if singleLetter.isLetter == true {
      print("Is letter")
      return true
     }else {
     print("Is not letter")
     return true
    }
    }


hope that helps,

DMG

Thanks for that, DarkAnGth. I've to put the coding aside but will get back to it in a day or two. I'll let you know how I get on.

Cheer, DMG. I'll have a crack at it again in a day or so and let you know if I have some sucess.

Thanks again, that was a great help. I think I understand it better now. I have one more question. I noticed that the backspace is treated as a character, as not a letter, so it gets replaced just like any nonLetter character. I did some searching and found this:


if text == "" && range.length > 0 { //Backspace

return true

}
It works nicely in exempting the backspace from being replaced, so that it still functions as a backspace. I was wondering though if there is a better way of identifying the backspace key. Perhaps it has a unique identifier or something? I have searched Apples documentation but haven't found anything specifically about it.

Cheers again, DarkAnGth. I have made some good headway, but I'm wondering about options for turning off the ability to paste. Do you know of a simple straightforward way to turn off that function? A number of possibilities show up on Stackview, but I find that stackview often has solutions that are years old and outdated. The ones I have tried are not working. I have scrolled through all the methods in UITextView but can't see anything that appears designed to switch it off.

Hi! To disable some actions you will need to create a subclass of UITextView, where you should override canPerformAction, like:

class MyTextView: UITextView {

  override func canPerformAction(_ action: Selector, withSender sender: Any?) -> Bool {
       if action == #selector(UIResponderStandardEditActions.paste(_:)) {
            return false
       }
       return true
  }

}


(and of course use this MyTextView instead of default UITextView in your project)

Generally seems what you need can be achieved without disabling paste functionality in such way:


func textView(_ textView: UITextView, shouldChangeTextIn range: NSRange, replacementText text: String) -> Bool {
  let resultingText = (textView.text as NSString).replacingCharacters(in: range, with: text)

  let hasNonLetterInResult = resultingText.first(where: { !$0.isLetter }) != nil
  if hasNonLetterInResult {
       print("Non-letter appeared")
       return false
  }

  print("All characters are letters")
  return true
}

hi,


my reply above was intended only to point out when shouldChangeTextIn was called and what you were looking at -- that was the source of your confusion -- rather than with about how you might use any code in practice.


i would not want to do any special-case handling for a backspace character. rather than think of the shouldChangeTextIn method as a "see what was typed" method, it's a more general method that tells you "i'm going to replace the current selection (specified by the range) with something (the replacementText), and is that OK?"


that means that proper backspace-handling is already built in for free:

  • if there is a non-empty selection range, pressing a backspace/delete will call shouldChangeTextIn to tell you "i'm going to replace the current range with an empty string," effectively asking if it's OK to delete the current selection.
  • if there is an empty selection range and the backspace/delete is pressed, i'm guessing that the selection range passed to by shouldChangeTextIn has already been set to include the single character preceding the selection point, effectively asking if it's OK to delete the character in front of the selection point.


if you have more extensive processing requirements, you could follow DarkAnGth's suggestion that you look at the effect the replacement will have, should you allow it to go through with


let resultingText = (textView.text as NSString).replacingCharacters(in: range, with: text) 


now you can inspect what will happen, returning true if you like it, and if you don't like the result, return false.


hope that helps,

DMG

Thanks for that. It works well. I don't have a full grasp of that shorthand yet. I'll have to look into it.

Thanks. For the app that I'm building I have very specific requirements for one particular textview, so my aim is to limit the user to only the characters that are allowed, which will eliminate any possibility of error. DarkAnGth's code works well. If the shouldChangeTextIn method is not really built for that purpose, is there another method maybe that is more suitable? It seems to be working well though, so far without any hiccups.