UIScrollView scrollEnabled does not stop two finger gestures - iphone

The UIScrollView property scrollEnabled stops one finger gestures but multiple finger gestures seem still to work albeit with the error message
Ignoring call to [UIPanGestureRecognizer setTranslation:inView:] since
gesture recognizer is not active.
Is this bad? Am I doing something wrong? canCancelContentTouches seems to stop the action but the error message remains. Can I thus ask
should I worry about the the error message?
is canCancelContentTouches the best way to cancel all scroll actions?
what am I doing wrong?

Your question is a bit unclear, but basically scrollEnabled is only designed to stop scrolling of the scrollview itself, not interaction with content inside the scrollview.
If you want to stop people from interacting with content within the scrollview, using a UIGestureRecognizer and its delegate to intercept the gestures and ignore them seem like the way to go.

Related

iPhone SDK - UIButton on a UIScrollView touch interactions

I have a UIScrollView which is doing some custom pinch zooming. To do this I subclassed UIScrollView, the overwrote the touch methods touchesBegan, touchesMoved, and touchesEnded. Everything works well and as expected.
My problem comes when I try to add a series of UIView subviews, I can only detect taps on my UIScrollView when the UIView UserInteractions is set to NO. I would like to be able to continue to detect two finger taps on my UIScrollView, AND a single finger tap on any of my UIView subview.
Is this possible?
I've tried countless number of ways with little help. Does anyone have any experience in this?
Cheers,
Brett
Apple's documentation for UIScrollView explains how it does it:
it temporarily intercepts a touch-down event by starting a timer and, before the timer fires, seeing if the touching finger makes any movement. If the timer fires without a significant change in position, the scroll view sends tracking events to the touched subview of the content view. If the user then drags their finger far enough before the timer elapses, the scroll view cancels any tracking in the subview and performs the scrolling itself.
There are a couple of methods for interception given as answers to this question: How to make a superview intercept button touch events?
Any UIView with userInteractionEnabled will block touches from reaching views under it. You may have to rethink how you are structuring your layout. Or subclass the UIView to change how it's handling touches.

How to filter touch events for a UIScrollView?

I have a view that displays a PDF. It should be zoomable, so I also created a UIScrollView, and in its delegate I implemented viewForZoomingInScrollView to return the PDF view. So far so good.
However, when the user reaches the edge of a zoomed PDF page, I'd like to flip to the next page. Sounds easy, yet I can't seem to figure out how to do it.
I've tried some different approaches:
Using scrollViewDidScroll to detect if scrolling has reached the edge. The problem here is that if zoomScale is 1, and therefore scrolling is not possible, then this function is never called. But the UIScrollView still swallows all touch events, so I also can't detect reaching the edge in touchesMoved. Setting canCancelContentTouches to NO when not zoomed is not an option, as that would also prevent zooming in.
Subclassing UIScrollView, and forwarding some of the touch events to the next responder. Unfortunately when UIScrollView detects a drag operation and cancels the touch, touchesMoved and touchesEnded are not called even for the UIScrollView subclass anymore. Again, setting canCancelContentTouches to NO is not good, as that would also prevent some desired UIScrollView functionality.
Creating a transparent view on top of the scroll view (as a sibling of it), so that this view gets all touch events first, and then forwarding some of the touches to the scroll view. Unfortunately the scroll view doesn't respond to these calls.
I can't use touchesShouldCancelInContentView, becasue that doesn't get the actual touches as an argument, and whether or not I want the scroll view to handle the touch event also depends on the properties of the touch event itself (eg. a touch movement in a direction in which we're already at the edge should not be cancelled by the scroll view, but a movement in the other direction could be).
Looks like whatever UIScrollView is doing is not initiated from touchesBegan / touchesMoved, but instead it gets some notifications about the touches way before that. Possibly in some undocumented way that I can't intercept, nor reproduce.
So is there any way to get notified about all touch movements done over a UIScrollView, while still being able to use (when certain conditions apply) the UIScrollView for zooming and scrolling?
Ok, so here's what I did in the end:
Leaving all scrolling and zooming up to UIScrollView, and handling page turning in the UIScrollViewDelegate's scrollViewDidEndDragging:willDecelerate: is almost a solution, except that this function is never called if the whole content is on-screen, so dragging / scrolling is not possible.
Swipes in this case are handled in a ViewController's touchesBegan / touchesEnded functions, but for this to work, we need to make sure that the UIScrollView does not cancel these events. However, in other cases the UIScrollView should be able to cancel touches so that it can do zooming and scrolling.
The UIScrollView should be able to cancel touches if:
Scrolling is possible (and needed) because the whole content doesn't fit on screen (zoomScale > 1 in my case),
OR
The user touched the screen with two fingers, so that zooming in and out works.
When scrolling is not possible, and the user single-touched the screen, then touches should not be cancelled, and touch events should be forwarded to the view controller.
So I created a UIScrollView subclass.
This subclass has a property pointing to the ViewController.
Using the touchesXXX methods I keep track of the current touch count.
I forward all touch events to the ViewController.
And finally, I've overridden touchesShouldCancelInContentView:, and return NO when zoomScale <= 1 and touchCount == 1.

How does UIScrollView steal touches from its subviews?

I'd like to be able to create my own container subclasses of UIView which can react first to touches, before their subviews. This is tricky because normally a subview receives touch events (via touchesBegan: etc) before superviews. How does UIScrollView reverse this?
To be clear, I am not asking how UIScrollView behaves. I understand what it does, and how you would normally use it. I'm asking about how I could cleanly implement my own version of this -- not because I want to, but because I'm trying to build reusable container views that take advantage of similar behavior.
You can implement hitTest:withEvent: method in your UIView subclass.
This method gets called to check what subview must receive the touch event, so you can perform some action there before it actually happens. You can also change subview that must receive event.
[Edit: Okay, I somehow read the question as the exact opposite what was actually asked. I'll leave this here in case someone is curious but it's not relevant to the actual question -- TechZen]
A scroll view has move both horizontally and vertically so it has to sample all touches to see if they are scrolling touches.
For example, you have a slider in scrollview that scrolls horizontally. The user touches the slider. Is the touch intended to slide the slider or is it the start of a horizontal scroll? Which should have precedence?
Even though programmatically, the responder chain moves from the bottom most view (top most visually) up the chain until it finds an object that deals with it, behind the scenes the processing of a touch's location goes from the top most view (which is the app window) downward. Most views simply forward this information to subviews who then activate then send an event up the responder chain to be handled. Because scrolling the entire view is a special event that doesn't really belong in the ordinary responder chain, a scrollview intercepts the down pass of the touch's location in order to determine if it needs to scroll before it passes the touch locations to subviews.
Here is a very simply way, yet effective
UITapGestureRecognizer *gestureRecognizer = [[UITapGestureRecognizer alloc] initWithTarget:self action:#selector(hideKeyBoard:)];
gestureRecognizer.delegate = self;
[scrollView addGestureRecognizer:gestureRecognizer];
-(void) hideKeyBoard:(id) sender
{
// Do whatever such as hiding the keyboard
}

Is it possible to disable the dragging waiting time in an UIScrollView?

When the user touches into an UIScrollView and wants to scroll, there is a little delay. UIKit tries to find out if the user wanted to touch the content in the scroll view or if the user wanted to scroll. So it waits a moment if the finger moves far enough, and then starts scrolling. Could I tell it that it must start scrolling with no delay? I have a situation where the content really doesn't care at all about touches, so the UIScrollView doesn't have to wait, it can immediately start scrolling.
I think you're looking for the delaysContentTouches property on UIScrollView. Setting that to NO should cause the UIScrollView to immediately start processing touch events.

How can a superview interecept a touch sequence before any of its subviews?

I have a view hierarchy that is layed out as follows:
parentView
scrollView
contentViewA
containerView
contentViewB
contentViewC
I want contentViewB to respond to touches. Unfortunately scrollView makes this almost impossible because it tries to ingest the touches itself making touch response of contentViewB spotty.
So, instead, I want to intercept all touches in the parentView, manipulate contentViewB directly, and then pass the touches on to scrollView so it can do its thing.
Can someone please show me the correct way to pull this off?
Thanks in advance.
Cheers,
Doug
UPDATE:
I did a bit more digging and found the property canCancelContentTouches which seems to work wonders. I'm using IB so I unchecked "Cancellable Content Touches" in IB - first tab of the Scroll View Attribute Inspector. Now, when I run the app, touches appear to be arriving at contentViewB reliably.
Here's how the UIScrollView docs describe this property:
Discussion
If the value of this property is YES and a view in the content has begun tracking a finger touching it, and if the user drags the finger enough to initiate a scroll, the view receives a touchesCancelled:withEvent: message and the scroll view handles the touch as a scroll. If the value of this property is NO, the scroll view does not scroll regardless of finger movement once the content view starts tracking.
Rather opaque huh? Anyway, it seems to work.
To stop the scroll view from intercepting the touch events, set the userInteractionEnabled property like so:
scrollView.userInteractionEnabled = NO;
Another way of doing this is to add another subview to your ui so it looks like so :
parentView
scrollView
contentViewA
containerView
contentViewB
contentViewC
touchGrabber
and, in touchGrabber, detect all the touches that you want (by subclassing UIView)
This is more complicated than Phil Nash's solution but has the advantage that you can add/remove other views from your parentView without having to deal with their userInteractionEnabled value - this is useful if you have a third party library adding views for example.
However, if you definately only going to have the scrollView, Phil Nash's answer is the way forward!
Thanks,
Sam