Allow cursor selection anywhere in UITextView - ios

I have a single UITextView, which lets you write multiple lines of text.
How can I let users select letters in between words without a long press?
Currently, when using the touchscreen to select characters between a word, the cursor either goes the start or end of the word:
Example: clicking on 'c' moves cursor to end of word
Example: clicking on 'a' moves cursor to start of word
Desired cursor behavior: Selection anywhere in text
The feature of allowing text selection anywhere (without the long press + magnifying glass) is found in many code editor apps for iOS, such as Coda and Pythonista.

This is not a difficult challenge to achieve.
I will preface that the issue with this approach is consistency. Remember that your users are using their fingers, rather than stilii. A finger has a large surface area, and hitting a precise pixel zone is quite difficult, especially with small text. I would suggesting playing not in the simulator, where you have a precise pointing device, but on a device.
The first challenge is to disable the default tap behavior or UITextView. This is not a difficult task - you can either "attack" the problem at the touchesBegan:withEvent: level, where you would have to understand what these touches are (single tap vs. pan vs. long press), or at the gesture recognizer level, where you'd disable the private gesture recognizers of the text view which specifically handle cursor movement in case of tap (vs the other touch types). I've done the latter for various projects, and it is possible. You could also try the approach without disabling the default behavior, but then the cursor may flicker. Test and decide.
Now to achieve what you need. Obtain the point of touch somehow (either using UIResponder API or gesture recognizer). Remember, the text view is a scroll view which includes a large subview where the content is drawn into. You have to convert this touch point, from the text view coordinate system, to the internal view's coordinate system using the convertPoint: API. Once you have that, you can use the text view's layout manager to obtain the character index at the point of touch:
NSUInteger chIdx = [self characterIndexForPoint:touchPoint inTextContainer:self.textContainer fractionOfDistanceBetweenInsertionPoints:NULL];
You can use this index to set the cursor of the text view using the selectedRange property.

Related

How to get correct screen coordinate from UITapGestureRecognizer while VoiceOver is on

I'm currently working on an interactive view that relies heavily on the user's touch location. I have found that there are a few ways to interact with the UITapGestureRecognizer while VoiceOver is on, but when I tap my point the values given are very wrong. I've looked elsewhere, but my use case is outside of the norm so there is not a lot to tell me what is going on. Has anyone experienced this before?
I am aware that I can change accessibilityTrait to UIAccessibilityTraitAllowsDirectInteraction which will give me the correct screen point when used, but I would like to know what is causing this issue at the very least for the sake of knowledge. To interact with the UITapGestureRecognizer I either double tap or do a 3D touch by pressing on hard on the screen. The ladder method doesn't work for the tap gesture but will work for the pan gesture.
This is the only line I use to get my screen points. My map view is a UIImageView
CGPoint screenPoint = [tapGesture locationInView:map];
I'm using a map of a building and I try to tap the same corner or landmark for my testing. I know I can't hit the same exact point every time, but I do use a stylus and I can get pretty close.
Without VoiceOver on I would get the result: (35.500, 154.363)
With VoiceOver on and tapping in generally the same spot, I get : (187.500, 197.682)
The point I am using to test is on the left side of the screen and the result from VoiceOver being on is in the middle of the screen. I believe the y-axis value may have changed because of my tool bar's size, but I have no idea what is throwing off the x-axis value. If more information is needed let me know.
UPDATE: Upon further investigation, it turns out that the UITapGestureRecognizer will always return (187.500, 197.682) no matter where I touch in the map view when VoiceOver is on. That point seems to be the middle of the map view. Oddly enough though, the UIPanGestureRecognizer will give me the correct (x,y) for my view if I use the 3D touch while VoiceOver is on.
On a side note not relating to the problem at hand, it seems if I use the accessibility trait UIAccessibilityTraitAllowsDirectInteraction the method UIAccessibilityConvertFrameToScreenCoordinates returns a frame that is higher than my view. It works fine if I do not change the trait.
Your problem may deal with the reference point used when VoiceOver is on.
Verify what your point coordinates are referring to : view or screen coordinates ?
I suggest you take a look at the following elements :
accessibilityFrame
accessibilityFrameInContainerSpace
UIAccessibilityConvertFrameToScreenCoordinates
According to your project, the previous elements may be interesting to get your purposes.

ios voiceover slider double tap and hold, but for custom view

I've created a custom view that acts like a UISlider - there is a "track", and a handle to "grab" to change the value. For particular reasons, I can't just make it a subclass of UISlider. I'm trying to make this slider as accessible as possible with VoiceOver. I have accessibilityIncrease and accessibilityDecrease on my custom view that handle single finger drag up and single finger drag down. This changes the value of the slider by 10% at a time.
However, I'd like to allow more fine grained control, just like a non-VoiceOver slider. By default , UISlider has double tap and hold, and you can drag up/down to "pan" the slider. I'd like to add exactly that to my custom view, but I can't find the correct incantation to handle the double tap and hold gesture.
Is there something I can do to mimic the double tap and hold gesture from UISlider on my custom view?
Thanks very much!!!
If you want to implement this kind of new gesture for VoiceOver users, just forget it.
The recommended gesture for this kind of UI control is definitely the implementation of adjustable value as you already did apparently.
I don't think it's a good idea to try and implement new VoiceOver gestures in an application because its users have their habits and they may be totally lost with your customed control if they cannot handle it unless you add an hint to explain but that's definitely not what I recommend anyway.
Otherwise, you could take a look at the pass through concept introduced in the What's New in Accessibility WWDC 2017 video that deals with the same idea but for a panning gesture...

Does NSAttributedString's enumerateAttribute method's block pass character ranges or glyph ranges?

tl;dr: if I call NSAttributedString's enumerateAttribute:inRange:options:usingBlock: method, does the range argument to the block pass a glyph range or a character range?
I have a text view in my app that needs to behave like Notes.app in that when it's not in edit mode, it should show hyperlinks (and they should be tappable), and it should enter edit mode on tap. UITextView doesn't show hyperlinks when editable, but if it's not editable then it won't gain focus on tap by itself. Until iOS 11 this could be solved by using a UITapGestureRecognizer on the text view to trigger editing, but it seems something (maybe with drag and drop?) changed how gesture recognizers work with UITextView (rdar://33009324).
My new and improved solution is a custom UIGestureRecognizer that fires if it sees a tap that is not on a hyperlink, and making all other recognizers on the text view to require it to fail before firing. This is implemented by processing touches, getting their location in the text view, getting the character index for that point, enumerating the NSLinkAttributeName in the text view's textStorage, and checking if my character index overlaps with any of the hyperlinks I get back. If so, then the user tapped on a link, and this recognizer fails.
However, if the character index of the tap does intersect with a link, I need to make sure the tap actually intersects with the link's rect, because it could be that this link is the last thing in the text view's content and the touch is actually below the last line of text. I can do this by getting the rect for the range of the link and checking if it intersects with my touch point. I can use boundingRectForGlyphRange:inTextContainer: for this, but this leads to my question: is the range I have for the link a character range or a glyph range? Do I need to convert it via glyphRangeForCharacterRange:actualGlyphRange: first?
It returns a character range. NSAttributedString doesn't know anything directly about glyphs. Converting characters to glyphs can't happen until layout is performed, and NSAttributedString doesn't include layout information. For example, an NSAttributedString doesn't know how wide an area it will be drawn into, and without that, you can't know where line breaks will be, and without that you can't decide where to put ligatures.
NSAttributedString knows nothing glyphs, and yes, if you need to know where the glyphs are in a TextKit stack such as that of a UITextView, you ask the layout manager to convert for you.

iOS place cursor after word in UITextView

I have a string in a UITextView.
NSString *str = #"Hello world. What #are you #doing ?"
When I tap on the UITextView the cursor goes where I tap on the string. But I need that, suppose I tap on any character of that textview, the cursor will automatically goes to the end of that word. For e.g, I tap on the character "e" in "Hello", then the cursor will place after "Hello".
How can I do this?
You have to understand the concept and underlying structure of a touch device.
Let me explain.
When you have a textfield/ textview, inside a view and your keyboard is not showing ie. textfield is inactive. The superview might have any number of gesture recognisers other than the textfield.
Hence the OS just detects the touch which will make the UI element become firstResponder, or rather Active.
After the textfield is active, OS recognises that the other gesture pattern will come into play in this moment. And in this moment you can tap in any position (or Long tap) and then place ur cursor at any character you want.
Hope you could understand.
this should be the default behavior of textview. You not need to do any additional setup for that! If you will long press then only your cursor go at particular character otherwise it will go to end of the word. this is the default behavior.

Add tap gesture recogniser on a specific word in a UITextView

I have a UITextView with some text. This text is an Attributed String(NSAttributedString). There are certain portions of the text that i have set to bold, and want to add a TapGestureRecogniser to those specific words only.
Till now, i have been using the textViewDidChangeSelection method of the UITextView delegate. But this is causing issues in other parts of the project.
Is there a more direct approach to this ?
You can only add a GestureRecognizer to a view, not to some words.
It's a quite complex task, there's no easy solution for it.
I can think in some approaches, for example:
Place a transparent view on top of the bold words to get the Tap.
Detect the Tap in all the UITextView, and then calculate based on the position of the touch and the position of the bold words if it hit one.
Both options requiere a lot of code and a lot of edge cases where it can fail.
As I said, it's a really complex situation, you may want to keep using textViewDidChangeSelection and fix the issues, we can help you.

Resources