UITextView delegate textViewDidChangeSelection is called twice - ios

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:
Write some lines, for example:
Line 1.
Bla bla bla
line 3
Go to the end of line 1.
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.

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.

Related

Swift TextView detect previous line on backspace

Problem:
I need to detect when backspace is tapped and goes back to the previous line.
How can we detect a backspace that brings us back to a previous line on a textview, NOT a new line but the previous line.
For example: We can detect a new line by the given code
if (text == "\n"){
print("new line")
}
We can detect a backspace.
if (range.length == 1 && text.isEmpty){
print("backspace pressed on keyboard")
}
Pseudo code: How can we detect a backspace that brings us back to a previous line on a textview, NOT a new line but the previous line.
func textView(_ textView: UITextView, shouldChangeTextIn range: NSRange, replacementText text: String) -> Bool {
if (range.length == 1 && text.isEmpty){
print("backspace pressed on keyboard")
if (backspace brought textview cursor back to its previous line){
}
}
Inputs on Keyboard:
return pressed -> bring us to new line
backspace pressed -> returns us to prev line
Given your overly simplistic examples you can check to see if the current text in the text view at the changed range is equal to a newline character.
if (textView.text as NSString).substring(with: range) == "\n" {
Keep in mind that text in a text view can wrap without ever entering a newline character. That may or may not be important for your use case. And this also means that the text can "unwrap" without deleting a newline character.
You seem to be assuming that the cursor is at the end of the text. Not sure how you want to handle the user deleting somewhere in the middle. Or what about a user selecting a bunch of text and deleting?
Also note that your check for a backspace isn't really a check for a backspace. That condition can also be met if the user selects one character and then selects the Cut menu.

Ignore space after predictive text selection in UITextView

I found something similar to my problem checkout here but it does not work for me: link
Thid UITextView delegate method calls twice If we tap on predictive text
1st call insert text 2nd call insert space after if
func textView(_ textView: UITextView, shouldChangeTextIn range: NSRange, replacementText text: String) -> Bool
I found one hack:
if text.count > 1 { return false }
But this logic failed when user tap on single character predictive text
I am doing some customizations in this method e.g. mentions support so after doing some work I update attributedText of TextView on mainThread so before setting attributedText to the textView this method get call for space in predictive text selection case and at that time previous text is not setup yet so it will insert just space.
Two things comes in my mind, somehow handle the call of this method that it should not call twice until text is set to textView
Second somehow ignore the space after predictive text.
if text == " " { return false }
We also can not do this because after that user will not be able to insert spaces.
Predictive texts:

Detecting paste UITextView in textViewDidChange

I am creating a UITextView for use as a rudimentary IDE. When the user pastes text (code) I am modifying it so that it fits inline (correctly indented) with the other text. However, I am also coloring the text (attributedText) using the textViewDidChange method. As a result when I paste text, it is first inserted then adjusted inline. The process looks a bit weird especially for a large chunck of text. I was wondering if there is a way for me to catch that the text is being pasted in textViewDidChange to avoid running attribute changes until the text is modified. Note that coloring attributes in the of the textView shouldChangeTextIn method is not possible as I am implementing some custom behavior through the interaction of the two methods and adding attributes needs to be done at the textViewDidChange stage.
EXAMPLE SETUP
func textView(_ textView: UITextView, shouldChangeTextIn range: NSRange, replacementText text: String) -> Bool {
if (text == UIPasteboard.general.string) {
// MODIFY PASTABLE TEXT HERE
// INSERT MODIFIED TEXT
textView.replace(textRange, withText: newText)
// PREVENT OLD TEXT FROM BEING INSERTED
return false
}
}
func textViewDidChange(_ textView: UITextView) {
// MANAGE ATTRIBUTES
}
WEIRD BEHAVIOR

UITapGestureRecognizer on a text field not as expected

In my class I have 11 UITapGestureRecognizers in an array textViewRecognizer attached to 11 out of 100 UITextFields in an array boxArray. When a Textfield is tapped containing a UIGestureRecognizer it runs tappedTextView where I try to get the index of the first responder.
However, due to some weird ordering in how things are executed, the action function only gives me the first responder of the previous first responder to the one that was just tapped.
Also, I have to double tap to even select the text field I was going for! I need to use the tap function and not the text delegates so this has been a real headache.
I have...
#objc func tappedTextField(_ sender: UITapGestureRecognizer) {
for i in 0...99 {
if (boxArray[i]?.isFirstResponder)! {
if let index = boxArray.index(of: boxArray[i]) {
print(index)
break
}
}
}
}
in my viewDidLoad I have
for i in 0...10 {
textFieldTapRecognizer[i].addTarget(self, action: #selector(self.tappedTextField(_:)))
}
In my class I have
I want to set 11 out of 100 textFields to have this a tap recognizer depending on some conditions (I'm just going to use a regular for loop here)
for i in 0...10 {
boxArray[i]?.addGestureRecognizer(textFieldTapRecognizer[i])
}
Is there anyway I can get it to give me the actual first responder, after the tap was made?
Is there anyway to go around the double tap to select the text field that has a UITapGesture?
Any help is greatly appreciated.
Edited: properly named functions
It sounds like you want to remove the automatic editing behavior on a UITextView. You can grab more control over that with the textViewShouldBeginEditing(_ textView: UITextView) -> Bool UITextViewDelegate method, documented here.
If you return false for that method, this should avoid needing a double tap to get to your gesture recognizer. Depending on your use case, you can then "allow" the tap to go to the text view by returning true for the textView you want to be actually edited.
While I'm not 100% clear on the first responder part of your question, since the textView won't be grabbing first responder if it's not starting it's editing mode, this should address that concern I believe. Good luck!
I would add a Tag to my UITextView and set the UITextViewDelegate to my ViewController.
Then I would add the following Delegate method:
func textViewShouldBeginEditing(_ textView: UITextView) -> Bool {
print("Textview tag: ", textView.tag)
return false
}

textViewDidChangeSelection method thinks textView.text is empty

For various reasons, I need to access the text in a textview whenever a selection is changed, so I have implemented the delegate method textViewDidChangeSelection() to do so. For some reason though, when I try to access textView.text from within this method, sometimes it comes back as empty even when it's not. Take this bit of code for example.
func textViewDidChangeSelection(_ textView: UITextView) {
print("Called textViewDidChangeSelection")
if textView.text.isEmpty {
print("textview is empty")
} else {
print("textview is not empty")
}
}
Using this example in my code, I click on a textView that is NOT empty and sometimes it comes back with "textview is empty." It seems like it tends to happen most often immediately after re-running the simulator and clicking on any textView, but I've also seen it happen when just clicking on a textView for the first time (after having clicked on some other textView) or when I segue back to my UIView containing the textViews from some other view.
Does anyone know why this happens?
textViewDidChangeSelection(_ textView: UITextView)
is called when the text selection changes, not necessarily just when another textView is selected. In fact in the olden days the selectedRange property of the textView used to return zero (indicating an insertion) but now (according to Apple) the length of the selection range may be non-zero. When you click away from a textView your delegate method may be firing for the initial textView (the one you're leaving) which would give you the result you're seeing, if it's empty.
Try giving your textView tags and using:
func textViewShouldBeginEditing(_ textView: UITextView) -> Bool {
print("Selected \(textView.tag)")
return true
}
To see if it solves your problem. Of course tags are just a quick and dirty check and you'd probably use a more flexible textView identification in your app.

Resources