UITextView observe text changes - ios

I want to listen for every text change in UITextView. The setup is very trivial.
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
NotificationCenter.default.addObserver(
self,
selector: #selector(textViewDidChangeWithNotification(_:)),
name: UITextView.textDidChangeNotification,
object: nil
)
}
#objc private func textViewDidChangeWithNotification(_ notification: Notification) {
print("Text: \(String(describing: inputTextView.text))")
}
It works OK in most cases, but then I have found some UITextInput's black box magic.
Step 1: 'I' typed. We can see 'I' in the output.
Step 2: Important step. Select all text with double tap on the field.
Step 3: Select 'If' from word suggestions.
And there is no 'If' in the debuggers output. On the other side if the caret will be at the end of the 'I' word and we select 'If' output results are correct.
Is there any way to observe ALL text changes?
I can get text by using:
func textViewDidEndEditing(_ textView: UITextView)
but I need to observe all changes in real time. The other option I always see is to use:
func textView(_ textView: UITextView, shouldChangeTextIn range: NSRange, replacementText text: String) -> Bool
but this method is a bad practice to observe changes. If you type two spaces repeatedly iOS will replace first space with dot and obviously will not inform you about this action in this method and a lot of other problems with it.

OK, after a lot of research I've tried RxSwift because I thought that observing text in reactive paradigm framework should succeed in 100% cases. And it worked without any issues!
inputTextView.rx.text.subscribe(onNext: { string in
print("rx: \(string)")
})
So it seems that these guys have found the solution.
https://github.com/ReactiveX/RxSwift/blob/master/RxCocoa/iOS/UITextView%2BRx.swift
And here is the solution that gives you information about all text changes despite of auto correction, text selection, and etc..
class ViewController: UIViewController {
#IBOutlet weak var inputTextView: UITextView!
override func viewDidLoad() {
super.viewDidLoad()
inputTextView.textStorage.delegate = self
}
}
extension ViewController: NSTextStorageDelegate {
func textStorage(_ textStorage: NSTextStorage, didProcessEditing editedMask: NSTextStorage.EditActions, range editedRange: NSRange, changeInLength delta: Int) {
print("string: \(textStorage.string)")
}
}

You can use func textViewDidChangeSelection(_ textView: UITextView) to detect the change in selection ?

If you want to listen for every change shouldChangeTextIn is the way to go. You can write conditions to solve the problems associated with it

Related

Resigning a text view as first responder and making a text field first responder when pressing return

I have an app with a text view and a text field where the user inputs a question in the view and an answer in the field. I want it to resign the view as first responder when the user presses return and make the field first responder and I tried the code below but it doesn't seem to be working and I can't figure out why
func textView(_ textView: UITextView, shouldChangeTextIn range: NSRange, replacementText text: String) -> Bool {
if text=="\n"{
questionTV.resignFirstResponder()
answerTF.becomeFirstResponder()
return false
}
return true
}
I've tried the following code:
func textView(_ textView: UITextView, shouldChangeTextIn range: NSRange, replacementText text: String) -> Bool {
guard text == "\n" else { return true }
textView.resignFirstResponder()
textField.becomeFirstResponder()
return false
}
It works as expected.
I guess, the reason might be somewhere else. Do you have any more details to share?
Did you set text view's delegate? Do you stop at breakpoint inside shouldChangeTextIn method?
UPDATE:
In comments it came clear that you conformed to the wrong protocol. You need to conform to UITextViewDelegate instead of UITextFieldDelegate. You might want to keep UITextFieldDelegate conformance, if you need it for work with text field though.

Detect if "delete" button on iOS keyboard is pressed in empty UISearchBar

I am wondering if there is a way to detect when user press delete button or try to erase character in UISearchBar? I searched for quite a bit on Google and I tried couple of solutions where one put a whiteSpaceCharacter in the empty UITextField and the other use shouldChangeTextInRange delegate to do.
The one with empty space, the user can somehow know that there is a space there by just press hold and highlight the space so I think that was not a very good solution.
The one that use shouldChangeTextInRange does not work when the search bar is empty.
Another thing to note is that majority of the solutions I found was in Objective C and it would be great if there are some way I can do this in Swift. Or would be great if anyone can point me to the right path.
Don't know why you would wan't do delete when the searchBar is empty, but you can do it like this:
import UIKit
class ViewController: UIViewController, UISearchBarDelegate{
private var search : UISearchBar!
override func viewDidLoad() {
super.viewDidLoad()
search = UISearchBar()
search.frame = CGRectMake(0, 0, 200, 200)
search.delegate = self
self.view.addSubview(search)
}
func searchBar(searchBar: UISearchBar, shouldChangeTextInRange range: NSRange, replacementText text: String) -> Bool {
let char = text.cStringUsingEncoding(NSUTF8StringEncoding)!
let isBackSpace = strcmp(char, "\\b")
if (isBackSpace == -92) {
print("Backspace was pressed")
}
return true
}
func searchBar(searchBar: UISearchBar, textDidChange searchText: String) {
if searchText == "" {
search.text = " "
}
}
}
This takes use of the UISearchBarDelegate, and finds out if backspace is pressed, via the shouldChangeTextInRange function. If the string is empty, it reapplies a whitespace to make it continuously deletable. It's kinda tacky, but the only way I know of to do it, as it doesen't register when the field is empty otherwise.
Hope it's of any use.

Dismiss Keyboard for textview

I have been trying to get the keyboard during a textview to dismiss but it STILL doesn't change the return button's action.
func textView(textView: UITextView, shouldChangeTextInRange range: NSRange, replacementText text: String) -> Bool {
if text == "\n"
{
textView.resignFirstResponder()
return false
}
return true
}
I was following the tutorial at https://www.youtube.com/watch?v=IsnoS8_G2SU
By now i have essentially copied the code 100%.
P.S. the textfield's outlet is named Textviews
Please help!
EDIT: Someone marked this as duplicate so let me explain why it isnt - I already have done what it tells me to do, and I'm not sure why it doesn't work for me.
You have made everything correct, but there is one step missing.
In order to hide or show the keyboard via the responder you have to set the delegate otherwise it won't work.
You can do it for example in the viewDidLoad:
override func viewDidLoad() {
myTextView.delegate = self
}
If your textfield is Textviews then you should have
Textviews.resignFirstResponder()
I hope this helps

Change 'Return' button function to 'Done' in swift in UITextView

I would like to get rid of the "return" function of the keyboard while the user is typing, so there are no new lines, so instead I would like the 'return' key to function as 'Done' so it would hide the keyboard.
I am using a UITextView, that is editable, so the user is able to type their post, and post it to the main timeline, but since I have fixed cells, I don't want the user to be able to press 'return' and their post would be out of range of the timeline.
I found this that works with UITextField, but not with UITextView:
func textFieldShouldReturn(textField: UITextField!) -> Bool {
textField.resignFirstResponder() //if desired
return true
}
So I just wanted to know if there is a way to do that in a UITextView, or at least to be able to hide the keyboard if pressed return, instead of creating a new line.
You can set the return key type of the text field:
textField.returnKeyType = UIReturnKeyType.done
Update
You can definitely use the same approach to set the return key to "Done", as mentioned above. However, UITextView doesn't provide a callback when user hits the return key. As a workaround, you can try to handle the textView(textView: UITextView, shouldChangeTextInRange range: NSRange, replacementText text: String) delegate call, and dismiss the keyboard when you detect the input of a new line character:
func textView(_ textView: UITextView, shouldChangeTextIn range: NSRange, replacementText text: String) -> Bool {
if (text == "\n") {
textView.resignFirstResponder()
}
return true
}
I have tried many codes and finally this worked for me in Swift 3.0 Latest [April 2019] this achieved using UITextFields
The "ViewController" class should be inherited the "UITextFieldDelegate" for making this code working.
class ViewController: UIViewController,UITextFieldDelegate
Add the Text field with the Proper Tag number and this tag number is used to take the control to appropriate text field based on incremental tag number assigned to it.
override func viewDidLoad() {
userNameTextField.delegate = self
userNameTextField.tag = 0
userNameTextField.returnKeyType = .next
passwordTextField.delegate = self
passwordTextField.tag = 1
passwordTextField.returnKeyType = .go
}
In the above code, the "returnKeyType = UIReturnKeyType.next" where will make the Key pad return key to display as "Next" you also have other options as "Join/Go" etc, based on your application change the values.
This "textFieldShouldReturn" is a method of UITextFieldDelegate controlled and here we have next field selection based on the Tag value incrementation.
func textFieldShouldReturn(_ textField: UITextField) -> Bool
{
if let nextField = textField.superview?.viewWithTag(textField.tag + 1) as? UITextField {
nextField.becomeFirstResponder()
} else {
textField.resignFirstResponder()
return true;
}
return false
}
If you're working with a storyboard or xib, you can change the UITextView's Return button to 'Done' (or various other options) within Interface Builder, without the need for any setup code. Just look for this option in the Attributes inspector:
From there, you just pair it up with the UITextViewDelegate code that others have already provided here.
Swift v5:
extension ExampleViewController: UITextViewDelegate {
func textView(_ textView: UITextView, shouldChangeTextIn range: NSRange, replacementText text: String) -> Bool {
if (text == "\n") {
textView.resignFirstResponder()
}
return true
}
}
And then, in your viewDidLoad() method:
exampleTextView.delegate = self
Working in Swift 4
Add this in viewDidLoad().
textField.returnKeyType = UIReturnKeyType.Done
Add this anywhere you like.
extension UITextView: UITextViewDelegate {
public func textViewDidChange(_ textView: UITextView) {
if text.last == "\n" { //Check if last char is newline
text.removeLast() //Remove newline
textView.resignFirstResponder() //Dismiss keyboard
}
}
}

How to trigger textViewdelegate on programmatical UITextView edits

If I am typing with the keyboard, then textViewDidChange and shouldChangeTextInRange are always called. But when I change the textView programmatically, the delegate methods aren't called. How do I get a programmatic change in textView to trigger delegate methods?
Update
Is there some other way for simulating keyboard input programmatically?
I solve this issue in my code using the insert method instead to change the value of the text string.
textView.insertText("Your Text")
Just ran into this question a few years later but had a hard time finding other good answers, so I want to demonstrate Satheesh's technique answered here with greater detail that worked for me for future readers and people that experience a similar issue.
Technique
First, add an observer to the textView:
let property = "text" //can also be attributedText for attributed strings
self.textView.addObserver(self, forKeyPath: property, options: NSKeyValueObservingOptions(rawValue: 0), context: nil)
Next, override the observeValue function:
override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
if object as? NSObject == self.textView {
//your code here
}
}
The observeValue function will run after the property value of the textView you added the observer to has been changed. Note that when you programmatically set the text of the textView like:
textView.text = "programmatically assigned text"
The shouldChangeTextIn delegate function is ran before the observer is called. As a side note, if you are using attributedText as the property, I would also recommend setting the property in that function and returning false:
func textView(_ textView: UITextView, shouldChangeTextIn range: NSRange, replacementText text: String) -> Bool {
let newString = NSString(string: textView.text!).replacingCharacters(in: range, with: text)
textView.attributedText = createAttributedString(text: newString)
return false
}
This ensures that you do not duplicate the text you are inputting into the text and attributedText properties. You can also use the delegate function to reject input, such as new lines, and it will not call the observeValue function. To re-iterate, that function is only called after the shouldChangeTextIn function returns true or false.
If there are any mistakes here or anything else someone else would like to add, feel free to let me know.
Happy programming!

Resources