Requesting caretRectForPosition: while the NSTextStorage has outstanding changes - ios

I've been recently getting the error:
requesting caretRectForPosition: while the NSTextStorage has oustanding changes {x, x}
* "Oustanding" is literally what it says, and is not my typo.
This is being called when I am iterating through the NSTextStorage of a subclass of NSTextView with the enumerateAttribute() method and manipulating the NSTextAttachments in the text view after every change in the text view.
func manipulateText() {
let text = customTextView.textStorage
text.enumerateAttribute(NSAttachmentAttributeName, inRange: NSMakeRange(0, text.length), options: NSAttributedStringEnumerationOptions(rawValue: 0)) {
//
}
}
extension ThisViewController: UITextViewDelegate {
func textViewDidChange(textView: UITextView) {
manipulateText()
}
}
Questions such as this seem to be online, but I have yet to find any occurrences of this and seems to be relevant to iOS 9 only.
This only happens when using a physical keyboard on iPad.

This happens if you call caretRectForPosition (or any method that calls that like firstRectForRange) while the text storage has edits.
I was able to prevent these logs by deferring some stuff until after endEditing is called in the NSTextStorage and dispatch_async to the main queue to do my work. There aren't any visible UI flashes or anything as a result of the async.
There has to be a better way to solve this, but this is all I could figure out.

Related

UITextView delegate textViewDidChangeSelection is called twice

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.

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
}

Delegates VS IBAction for handling events iOS

I am new to swift and new to iOS development. I am currently struggling to understand why delegates are used to handle events that happen in the UI.
So far, in the tutorial I am working through, I have only ever done things like the following code segment to handle UI events:
#IBAction func fahrenheitFieldEditingChanged(_ textField: UITextField) {
if let text = textField.text, let value = Double(text) {
fahrenheitValue = Measurement(value: value, unit: .fahrenheit)
} else {
fahrenheitValue = nil
}
}
But now the tutorial I am working through is having me use a delegate to to handle another UI event from the same text field. Why is it done this way? What is the point of using delegates rather than just write actions to handle the events?
func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
let existingTextHasDecimalSeparator = textField.text?.range(of: ".")
let replacementTextHasDecimalSeparator = string.range(of: ".")
if existingTextHasDecimalSeparator != nil, replacementTextHasDecimalSeparator != nil {
return false
} else {
return true
}
}
Take a look at UITextViewDelegate. It'll become easier to understand.
Responding to Editing Notifications
func textViewShouldBeginEditing(UITextView)
Asks the delegate if editing should begin in the specified text view.
func textViewDidBeginEditing(UITextView)
Tells the delegate that editing of the specified text view has begun.
func textViewShouldEndEditing(UITextView)
Asks the delegate if editing should stop in the specified text view.
func textViewDidEndEditing(UITextView)
Tells the delegate that editing of the specified text view has ended.
Responding to Text Changes
func textView(UITextView, shouldChangeTextIn: NSRange, replacementText: String)
Asks the delegate whether the specified text should be replaced in the text view.
func textViewDidChange(UITextView)
Tells the delegate that the text or attributes in the specified text view were changed by the user.
There is more, but I won't copy/paste anymore you can see yourself. What you see here are things that you can tune in if you want.
If you want you can tune in and get the callback of when the user starts typing. Or you can tune in and get the callback of when the user ended his editing.
How can you find out when the user stoped or started using IBAction?!
Once* you set yourself as a delegate (textViewInstance.delegate = self, you can then choose to get any of these callbacks.
*To be 100% accurate, you need to do that, but also adopt the UITextViewDelegate protocol and then conform to it using the mentioned delegate callbacks
Delegate is a design pattern that allows you to easily customize the behavior of several objects in one central object.
Actions are merely user interactions.
It's not possible to replace delegate callbacks with actions. For instance, if you have a WebView which fails to load, then how does it inform your object using an action?
In your example, an action cannot return properties like shouldChangeCharactersIn and replacementString. You need a delegate for that.
Read more...
Delegate pattern (Apple docs)

The caret disappears or won't show up in UITextField

I'm having trouble with the caret of UITextfields.
Whenever I tap on the textfield the caret doesn't show, but otherwise the textfield is working properly. The problem is similar to the problem described here (already tried the solution written there as well).
I noticed that whenever I load a viewcontroller and tap on a textfield (or programatically make it the first responder) it does show a caret, but as soon as I tap another textfield or make a textfield resign its first responder status no textfield will show its caret anymore (unless I go back and reload the viewcontroller again).
Got no clue as of why this is happening. I am using a custom font throughout the app. But I'm not sure if that's what making the caret disappear, as I've used custom fonts before with no problem whatsoever.
EDIT:
I think I've at least found why the problem occurs. It is related to me overriding the becomeFirstResponder and resignFirstResponder.
My code looks like:
extension UITextField {
open override func becomeFirstResponder() -> Bool {
let willBecomeResponder = super.becomeFirstResponder()
if willBecomeResponder {
backgroundColor = .red
layer.borderColor = .blue
}
return willBecomeResponder
}
open override func resignFirstResponder() -> Bool {
let willResignResponder = super.resignFirstResponder()
if willResignResponder {
backgroundColor = .blue
layer.borderColor = .red
}
return willResignResponder
}
}
Overriding those methods in the extension makes the caret disappear.
I'm thinking this most likely happens because UITextField itself does some...'caret-management' in it's own implementation of those methods.
More so because even return super.becomeFirstResponder() and no custom code in the overridden method makes the caret disappear.
My question therefore is; how can one solve this problem without making a custom UITextField subclass?
Calling super obviously just calls the UIResponder's implementation, but the docs specifically mention:
becomeFirstResponder()
You can override this method in your custom responders to update your object's state or perform some action such as highlighting the selection. If you override this method, you must call super at some point in your implementation.
So I need to call super I guess.
Alright I solved my problem by overriding isFirstResponder and altering the textfield based on the super.isFirstResponder there.
Looks like:
open override var isFirstResponder: Bool {
get {
let responder = super.isFirstResponder
backgroundColor = (responder) ? .red : .blue
layer.borderColor = (responder) ? .blue : .red
return responder
}
}
I did noticed a lot of calls being sent to isFirstResponder, so maybe this isn't the most efficient way (if altering the textfield is a heavy op).

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