How to trigger textViewdelegate on programmatical UITextView edits - ios

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!

Related

UITextView observe text changes

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

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.

How to respond to programmatic changes of a TextView text

The TextView delegate is set:
textView.delegate = self //self being a UITextViewDelegate
but the delegate method doesn't get called when the text is set programmatically
func textViewDidChange(_ textView: UITextView) {
print(textView.text)
}
How to respond to text changes without going reactive?
This is how UITextField (and most of UIKit controls) behave- doesn't trigger event when set programatically. It makes sense- lets you avoid recurring, infinite calls.
If you really want to be notify when text is changed programatically, you have to subclass UITextField and override text property (probably attributedText also). Then in didSet block call delegate method.
Don't forget that UITextField inherits from UIControl- I would also call sendActions(for:) to make target-action mechanism fire.
I just tried KVO on UITextView,
self.textView1.text = "You are working, but I will change you in 5 seconds"
Add your observer
self.textView1.addObserver(self, forKeyPath: "text", options: NSKeyValueObservingOptions(rawValue: 0), context: nil)
Trigger text change programmatically, just an example do it the way you want.
DispatchQueue.main.asyncAfter(deadline:.now() + 5) {
self.textView1.text = "Changed after some time"
}
Override the KVO method.
override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
if object == self.textView1{
//do your thing here...
}
}
FYI from Apple docs below
Note: Although the classes of the UIKit framework generally do not
support KVO, you can still implement it in the custom objects of your
application, including custom views.
https://developer.apple.com/library/content/documentation/General/Conceptual/DevPedia-CocoaCore/KVO.html
declare your text view variable with #objc dynamic
declare and hold a variable with type NSKeyValueObservation
use function observe(_:changeHandler:) bind your text view's text property, hold the return value with variable declared in step 2
observe changes in changeHandler
example:
#objc dynamic private var textView: UITextView!
private var observation: NSKeyValueObservation?
func bind() {
observation = observe(\.textView.text, options: [.old, .new]) { object, change in
print(object, change)
}
}
You have misspelled the delegate with textView with _textView
func textViewDidChange(textView: UITextView) { //Handle the text changes here
print(textView.text); //the textView parameter is the textView where text was changed
}
Put this in viewDidLoad
textView!.delegate = self

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
}
}
}

Resources