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;
}
Related
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.
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;
}
I'm building an iOS app that has a custom UIView upon a UIScrollView which in turn has a subview.
Here's the layout structure:
Note that the custom UIView(called "Detected Object Hint View") is not a subview of ScrollView, it's a sibling view of UIScrollView. And I want to respond to tap gesture on the custom UIView, so I've added UITapGestureRecognizer to the UIView, and it works for tap, but the UIScrollView will never get any touch events (not responding to scroll or zoom gesture).
I've googled a while, and a lot of people pointed out that in order for other view to respond to the touch events, I should implement the following method:
- (id)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
UIView *hitView = [super hitTest:point withEvent:event];
if (hitView == self){
return nil;
}
else {
return hitView;
}
}
But once I've added this method to my custom UIView, it will not respond to tap gesture either (of course).
So I'm wondering how can I handle the tap gesture on my custom UIView and pass the touch events to UIScrollView as well?
Big thanks!
I've implemented a paged scroll according to this technique
( iOS develop. How to extend UIScrollView's scroll event responding area? ) and it works just as intended.
The view that I'm scrolling is containing a couple of buttons and I want to be able to click not only those that are centered/paged into the scrollview but also those to the left and to the right of it. I cannot find any way to solve this but I'm not really an iOS-Jedi yet, hoping one of you are though :)
So as you can see from the screenie the UIScrollView is about a third of the width of the window, the contentsize of the UIScrollView is much larger: about 1500px and contains a lot of buttons added programmatically. The cool thing with this solution, and the part that actually works, is that the buttons:
1) are paged into the scrollview
2) are visible outside the scrollview (since "clip subviews" is unchecked for the scrollview) 3) the buttons are clickable when visible inside the uiscrollview.
BUT what doesn't work is simply this:
- the buttons currently being outside of the window does not receive "their" clicks when clicking on them, the events are instead forwarded to the underlaying (the white part of the window) view.
So,
I finally managed to solve this puzzle and the solution is divided into two
parts. The problem was, as you way recall, that the click events did not travel to the
buttons that were(visible) outside the UIScrollView. It turned out that the clicks were captured by the underlying view and that it is possible to manipulate their way to finding their target by bending the rules a bit regarding who got hit and thereby tricking the events to get passed where you want them. Not really sure if this is how it should be done but it solved my problem.. . :)
1) First one must override the following method in the bottom view
so that it returns the scrollview instead of itself when appropriate.
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event
{
UIView *view = [super hitTest:point withEvent:event];
if (view == self)
return [self scrollView];
return view;
}
2) The scrollView must override TWO methods to hand over the clicks to its contained objects.
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event
{
UIView *view = [super hitTest:point withEvent:event];
// Always return us.
return view ;
}
and
- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event
{
// We want EVERYTHING!
return YES;
}
Thanks a lot for you comments and willingness to help.
I ho
Inspired by the answer #tommys mentioned, it turns out that by overriding the hinTest method of a UIView and return the scrollView instead, you actually can detach the swiping of this UIView to the scrollView.
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event
{
UIView *view = [super hitTest:point withEvent:event];
// Doing this make you detached the swiping to the scrollView
if (view == self)
return [self scrollView];
return view;
}
So this UIView is acting like an extension scroll area of the scrollView, the idea is here. If you make the UIView mask over the scrollView and same size of the window, then swiping anywhere inside the window makes the scrollView scroll.
Here is the example, ExtensionScrollArea
Here's my version:
hit test in container
- (UIView *) hitTest:(CGPoint)point withEvent:(UIEvent *)event
{
if ( CGRectContainsPoint( self.frame, point ) && ! self.hidden ) // <-- *
{
if ( ! CGRectContainsPoint( scrollView.frame, point ) )
return scrollView;
}
return [super hitTest:point withEvent:event];
}
(*) This marked line is important if you are moving about or otherwise hiding your view, for instance if you have multiple views, each with their own scrollviews. If you don't have this line, you may be directing all your touches to an off-screen scrollview!
override in scrollview
- (BOOL) pointInside:(CGPoint)point withEvent:(UIEvent *)event
{
return YES;
}
(in the hitTest of the container, you can exclude additional frames within the if statement for default behaviour) :)
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]];
}
}
}