2

I have a library I’m building that needs to keep track of text entry into a UITextView in order to keep track of positions of certain sections of text. Most cases are handled fine within the textView(_:shouldChangeTextIn:replacementText:) method. The problem I’m having is that certain keyboards have character combinations that will actually change the character without calling the textView(_:shouldChangeTextIn:replacementText:) method. A simple example of this is if you choose a Vietnamese keyboard and type uw it will change to ú but the only calls to textView(_:shouldChangeTextIn:replacementText:) will be for the entry of u and w. The offset calculations will be incorrect at this point. As far as I can tell there’s no other method that I could use to know about this change. I’m not sure if this is a bug or not. The textViewDidChange method will be called but I was hoping to avoid hacky logic where I compare the previous and new text of the textView to determine any changes and adjust logic there. If anyone has suggestions I would love to hear them. Hopefully someone has seen and solved this problem. Thanks.

To clarify, I need to handle this situation using something other than subclassing.

Sample project: https://github.com/szweier/ShouldChangeTextInRange If you run the code above and choose the Vietnamese keyboard to type in uw you will see that the string generated in the textView(_:shouldChangeTextIn:replacementText:) method will be different from the text within the textView.

Steve
  • 921
  • 1
  • 7
  • 18

1 Answers1

0

UITextView or it's delegate doesn't have a shouldReplaceCharacters API as far as I know (and I'm not googling up anything useful either, at least for iOS -- it does exist for macOS though)

If I subclass UITextView, I can override insertText and detect what the true insertion point is:

class TestTextView : UITextView {
    override func insertText(_ text: String) {

        let selectedRange = self.selectedRange
        if let markedRange = self.markedTextRange {
            Swift.print("TestTextView inserting \(text) at \(selectedRange.location) & \(selectedRange.length) ; marked = \(markedRange.start) & \(markedRange.end)")
        } else {
            Swift.print("TestTextView inserting \(text) at \(selectedRange.location) & \(selectedRange.length) and no marked range")
        }
        super.insertText(text)
    }
}

When I add this subclassed TestTextView to a test project and turn on a VN keyboard. Here's what I see for uw typed in:

TestTextView inserting u at 17 & 0 and no marked range
TestTextView inserting ư at 17 & 0 and no marked range

while "e" & "b" typed sequentially looks like this:

TestTextView inserting e at 17 & 0 and no marked range
TestTextView inserting b at 18 & 0 and no marked range

My Test Project UI looks like this

Lastly, my answer is pretty simple and you may need to keep track of "marked text range", which comes into play with this multistage text input.

Michael Dautermann
  • 88,797
  • 17
  • 166
  • 215
  • Thanks for the response. Sorry if I didn’t get the delegate method name exact (I was typing from my phone and didn’t double check exact naming I meant https://developer.apple.com/documentation/uikit/uitextviewdelegate/1618630-textview . In any case for my situation I can’t subclass I’ve created a class that inserts itself as the delegate for the UITextView. I apologize for having not made that clear above. – Steve Mar 24 '19 at 11:31
  • What's your technical limitation for not being able to subclass? Subclasses of UITextView can still designate a `UITextViewDelegate` and use its protocol. – Michael Dautermann Mar 24 '19 at 11:33
  • I’m building an open sourced library that handles the addition/deletion of mentions into a textView. I avoided making it a subclass in order to provide more freedom to the user of the library. If they wanted to use, for example, a placeholder textView library that was a subclass of UITextView I wanted my library to remain usable. For what it’s worth the library is also essentially done and this was an issue that was reported recently. Refactoring into a subclass isn’t something I’m prepared to do either way. – Steve Mar 24 '19 at 11:39
  • You could try [method](https://medium.com/@abhimuralidharan/method-swizzling-in-ios-swift-1f38edaf984f) [swizzling](https://stackoverflow.com/questions/39562887/how-to-implement-method-swizzling-swift-3-0) `UITextView`'s `insertText`. – Michael Dautermann Mar 24 '19 at 11:42
  • Good point. I was just rereading your original solution and I’m wondering if the data received is correct. In the case where we insert u and then ú it seems to be saying it inserted u at position 17 with a length of 0 and the same for ú in that case, assuming I’m reading this properly, it’s saying that the end result is `”some text úu”` I would think the second insert log would say at 17 & 1 in order to replace. Am I wrong? It seems like none of these methods get quite enough information in this scenario. – Steve Mar 24 '19 at 11:49
  • I added a sample project that seems to show the issue relatively clearly. I've also created a bug report for Apple although I'm not sure whether or not my expectation of having that method called when the OS makes the replacement is a valid expectation. The docs seem to say it's only called upon user text entry. – Steve Mar 24 '19 at 13:28