Multiline UITextView in UIScrollView selection not working - ios

I have a multiline UITextView that sits in a UIScrollView. It's configured to be editable and selectable, so long pressing brings up the standard Select/Select All, etc menu. If I tap Select All I can only resize the selection on the first line. Once the selection changes to not include the first line the selection handles no longer respond to touch input.
If I select a single word on the first line, the left selection handle functions normally, but the right handle doesn't receive touch input.
Any ideas what might be causing this? This is very strange and I can't really figure out what's going on. I'm not overriding any gestures (that I can tell, anyway).

Ah, figured it out. The text view was inside another view, as expected, but I had custom hit detection on the parent view. In -hitTest:withEvent: in the case where I was in editing mode I was returning the text view itself instead of calling hitTest on the text view so it could determine if the selection view should be returned.
So, long story long, if you have a text view inside another view and are doing custom hit detection, make sure to call -hitTest:withEvent: on the UITextView as necessary so you don't lose selection capabilities.
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
// a text box is tappable if:
// a) obviously, the tap point is inside self
// b) if current mode is unselected, check to see if a label was tapped. Because the label stretches the width of
// the text box for layout purposes, check to see if the tap point lays inside with intrinsic content size of the
// label. If not, pass the tap to the next subview
// c) If the mode is already selected, ask super to return the appropriate view.
if (!self.userInteractionEnabled || self.hidden || self.alpha <= 0.01) {
return nil;
}
CGRect touchRect = self.bounds;
if (self.currentMode == TextBoxModeSelected) {
touchRect = CGRectInset(self.bounds, -22, -22);
}
if (CGRectContainsPoint(touchRect, point)) {
if (self.currentMode == TextBoxModeUnselected) {
return [self.readOnlyTextView hitTest:point withEvent:event];
}
else if (self.currentMode == TextBoxModeSelected) {
return self;
}
else if (self.currentMode == TextBoxModeEdit) {
CGPoint pointInTextView = [self.editingTextView convertPoint:point fromView:self];
if ([self.editingTextView pointInside:pointInTextView withEvent:event]) {
// PREVIOUSLY I WAS RETURNING self.editingTextView HERE
// self.contentView IS THE TEXT VIEW'S PARENT. LET IT DETERMINE THE
// SUBVIEW THAT'S BEING TAPPED. IN THE CASE OF SELECTION, IT'S UITextRangeView
return [self.contentView hitTest:point withEvent:event];
}
}
}
return nil;
}

Related

UITapGestureRecognizer not recognized because view is nested in multiple views

I have a custom view that I present by tapping on either a button on the view controller or by tapping on a button on tableview cell (table view is a child of the view controller)
To dismiss the custom view I want the user to be able to tap anywhere on the screen to dismiss it. However due to the many hierarchies of the view in view controllers. A simple UITapGuestureRecognizer isn't working. Is there any workaround for a case like this?
Create a subclass of UIView, call it MyTapView. Assign this class your parent view, which holds all your subviews.
Override in your class the following to intercept any touches made to your view instance.
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event
{
if (!self.clipsToBounds && !self.hidden && self.alpha > 0) {
for (UIView *subview in self.subviews.reverseObjectEnumerator) {
CGPoint subPoint = [subview convertPoint:point fromView:self];
UIView *result = [subview hitTest:subPoint withEvent:event];
if (result != nil) {
return result;
}
}
}
// use this to pass the 'touch' onward in case no subviews trigger the touch
return [super hitTest:point withEvent:event];
}
This method ignores view objects that are hidden, that have disabled user interactions, or have an alpha level less than 0.01. This method does not take the view’s content into account when determining a hit (feel free to modify this). Thus, a view can still be returned even if the specified point is in a transparent portion of that view’s content and now, after it has been overridden, receives touches outside the bounds.

Enable scrolling in a region for nested scroll views

I am working with nested scroll views as described here to create cross-directional scrolling.
https://developer.apple.com/library/ios/documentation/WindowsViews/Conceptual/UIScrollView_pg/NestedScrollViews/NestedScrollViews.html
Is it possible to define a region that the user can touch to scroll while still passing those touches to the nested scroll views?
Here is an example using the image from that documentation link. I only want the red region for UIScrollView A to be scrollable while still passing touches to UIScrollView B if the user scrolled on the right side.
The problem here is if I block touches using a method like -pointInside:withEvent: then the nested scroll views won't get the touch. I want the nested scroll views to accept scrolling in the entire view but the parent to only accept scrolling touches in part of it's view marked in red for this example.
You can overload method hitTest, in your parent view. This method should returns active scroll view.
And also you should write some custom code to manage touchable areas.
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event
{
UIView* result = [super hitTest:point withEvent:event];
if (result)
{
if ([self scrollChildWithPoint:point])
{
result = self.childScrollView;
}
else if([self scrollParentWithPoint:point])
{
result = self.parentScrollView;
}
else
{
result = nil;
}
}
return result;
}

iOS/ObjC: "Fallback" tap gesture recognizer?

I have a UIViewController subclass whose view will generally contain some number of UIButtons and other interactive elements which may have one or more gesture recognizes attached to them.
What I'm trying to do is provide some visual feedback in the event that the user taps on a part of the screen that is not interactive. In other words: when the user taps the screen anywhere, if and only if no other control in the view responds to the touch event (including if it's, say, the start of a drag), then I want to fire off a method based on the location of the tap.
Is there a straightforward way to do this that would not require my attaching any additional logic to the interactive elements in the view, or that would at least allow me to attach such logic automatically by traversing the view hierarchy?
You can override the pointInside:withEvent: method of the container view and return NO if there is an interactive element under the tapped location, and return YES otherwise. The case where you return YES will correspond to tapping on an empty location.
- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event
{
// Perform some checks using the CGRectContainsPoint method
// e.g. CGRectContainsPoint(oneOftheSubviews.frame, point)
}
A good alternative is using the hitTest:withEvent: method as in the example below:
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event
{
UIView *hitView = [super hitTest:point withEvent:event];
if (hitView != someView) return nil;
return hitView;
}
[super hitTest:point withEvent:event] will return the deepest view in that view's hierarchy that was tapped.
So you can check the type of hitView, and if it corresponds to one of your interactive subviews, return it. If it is equal to self, then it means that there isn't a subview under the tapped location, which in your case means that the user tapped on an empty area. In that case you'll do whatever you want to do and then return nil.

Automatic adjustment of UIScrollView content offset with custom UIControl

When a UITextField is added to a UIScrollview the scroll view automatically adjusts its contentOffset so that the view will not be obscured by the keyboard.
I have a custom UIControl which also presents a keyboard when it becomes the first responder by assigning its inputView property. The same scrolling behavior does not work. Is there a way to configure a UIControl such that a scroll view will keep it visible when the keyboard is presented?
My guess is that it could be possible by overriding a property defined in one of the protocols UITextField and other classes which this behavior conform to. But these can be a bit of a maze. Also note, the issue here has nothing to do with the scroll view's contentInset property. The scroll view can scroll to show the custom control, it just doesn't do it automatically when the control becomes the first responder.
It looks like this is handled by an internal private method that Apple utilizes [UIFieldEditor scrollSelectionToVisible] as noted on this blog: http://sugarrushva.my03.com/712423-disable-uiscrollview-scrolling-when-uitextfield-becomes-first-responder.html
It appears to do this by stepping back up through the view hierarchy and if it finds a parent UIScrollView, it scrolls the view to bring the UITextField into visible view. You'll need to implement the scrolling manually on your custom control when it becomes first responder, or handle it by introspecting the parent views.
I was pointed in the right direction by #markflowers.
Based on that, here's what I've written into the control to get the desired behavior:
- (BOOL)becomeFirstResponder {
if ([super becomeFirstResponder]) {
[self scrollParentViewToFrame];
return YES;
}
return NO;
}
- (void)scrollParentViewToFrame {
UIScrollView *scrollView = self.parentScrollView;
CGRect frame = [scrollView convertRect:self.bounds fromView:self];
[self.parentScrollView scrollRectToVisible:frame animated:YES];
}
- (UIScrollView *)parentScrollView {
return (UIScrollView *) [self closestParentWithClass:[UIScrollView class]];
}
Note that the frame attribute is not used in case the control is not a direct descendant of the scroll view. Instead convert the bounds to the scroll view's coordinate space.
The scroll adjustment is also needs to be performed after [super becomeFirstResponder] is called for it to interact properly with keyboard notifications that are being used to adjust the insets of the scroll view.
I defined the method to search for the closest parent scroll view in a UIView category which made it easier to recursively search up the hierarchy.
- (UIView *)closestParentWithClass:(Class)class {
if ([self isKindOfClass:class]) {
return self;
}
// Recursively searches up the view hierarchy, returns nil if a view
// has no superview.
return [self.superview closestParentWithClass:class];
}

How to support touch move across various objects?

I have some labels in my app like this...
What i need to do is, when clicking on a label, just I'm showing label name in the bottom of the screen. It works fine while clicking on each cell separately. But i want to show the changes even the user click on a particular label and move his finger on another label. That is, once he pressed on screen, where ever his finger moves, i want to trace those places and want to show the changes. How can i do this? Please explain briefly.
Thanks in Advance
By default, touch events are only sent to the view they started in. So the easiest way to do what you're trying to do would be to put all of your labels in a container view that intercepts the touch events, and let the container view decide how to handle the events.
First create a UIView subclass for the container and intercept the touch events by overriding hitTest:withEvent::
-(UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
// intercept touches
if ([self pointInside:point withEvent:event]) {
return self;
}
return nil;
}
Set that custom class as the class of the container view. Then, implement the various touches*:withEvent: methods on your container view. In your case, something like this should work:
-(void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event {
// determine which view is under the touch
UIView* view = [super hitTest:[[touches anyObject] locationInView:self] withEvent:nil];
// get that label's text and set it on the indicator label
if (view != nil && view != self) {
if ([view respondsToSelector:#selector(text)]) {
// update the text of the indicator label
[[self indicatorLabel] setText:[view text]];
}
}
}

Resources