How to implement UITableView like scrolling and cell tapping behavior? - ios

In UITableView, you can tap and hold on a table cell and cancels the tap and continue on with scrolling if you move your finger. How can this done with UIScrollView with a subview? I am able to make it so you can scroll and you can tap on the subview, but am having issue with the latter behavior - make scrollview continue to scroll if you move your finger while tapping and holding.

I assume you are using a UITapGestureRecognizer on the subview. The scroll view uses a UIPanGestureRecognizer for scrolling. You simply need to tell the tap recognizer not to recognize unless the pan recognizer fails.
If you are targetting iOS 5, this is very easy:
[self.tapRecognizer requireGestureRecognizerToFail:self.scrollView.panGestureRecognizer];
If you are targetting an older version of iOS, older versions of UIScrollView don't have the panGestureRecognizer property. Instead you have to search through the scroll view's gestureRecognizers array:
for (UIGestureRecognizer *recognizer in self.scrollView.gestureRecognizers) {
if ([recognizer isKindOfClass:[UIPanGestureRecognizer class]])
[self.tapRecognizer requireGestureRecognizerToFail:recognizer];
}

Related

iOS Swift 4: UIView in UIScrollView

I have a UIImageView with a tap gesture recognizer as a subview of a UIScrollView.
A.) If the UIImageView isUserInteractionEnabled=false the scroll view works fine (pinch zoom, scroll) but doesn't recognize the tap gesture on the image.
B.) If isUserInteractionEnabled=true I cannot start pinch zoom or scroll from the image but the tap gesture works.
How can I manage it to work (keep scrolling and zooming but recognize tap on content)?
Because UIScrollView has gestures within for handling scroll, pinch, it means when your UIImageView.isUserInteractionEnabled = true, the UIImageView' tap gesture take those touches and do not forward it to UISCrollView.
Here is the solution by implementing a UIGesture's delegate method: https://developer.apple.com/documentation/uikit/touches_presses_and_gestures/coordinating_multiple_gesture_recognizers/allowing_the_simultaneous_recognition_of_multiple_gestures

Panning objects on a UIScrollView

I'm working on an app where I have several UIView objects that are subviews on a UIScrollView object. I create the subviews programmatically and place them on the scroll view according to the properties of associated objects. The user is allowed to move these subviews around on the scroll view. Usually this works, but sometimes the scrollview grabs the pan gesture.
What I'd like to do is to suppress the scroll view gesture recognizer if the touch location is inside one of the subviews.
I can find the scroll view gesture recognizer by looking through the scroll view's array of gesture recognizers and looking for a UIScrollViewPanGestureRecognizer object. I assume there can only be one.
An idea I have is to make my view controller be a delegate of this gesture recognizer and then have the delegate suppress it if the touch is within the bounds of one of the subviews.
Is this the best way to handle this scenario, or is there a better way?
I've done something similar, described in my answer to my own question here.
How to get stepper and longpress to coexist?
Hmmm. Looks like it will be more difficult than I anticipated to recognize the scrollview's UIScrollViewPanGestureRecognizer. Any hints on doing this would be appreciated.
My idea doesn't work. In order to code my idea, I had to make my VC be the delegate of the scrollview's pan gesture recognizer. However, when I do that, I get this error:
*** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: 'UIScrollView's built-in pan gesture recognizer must have its scroll view as its delegate.'
Here is the code I used. In viewDidLoad I called a method which got the scrollview's pan gesture recognizer and set self as delegate (self.scrollViewPanGestureRecognizer is just a property to store it):
self.scrollViewPanGestureRecognizer = [self.scrollView panGestureRecognizer];
self.scrollViewPanGestureRecognizer.delegate = self;
I then implemented this delegate method:
-(BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldReceiveTouch:(UITouch *)touch
{
//Disable touch if touch location is in a subview.
BOOL enableGestureRecognizer = YES;
if (gestureRecognizer == self.scrollViewPanGestureRecognizer) {
CGPoint touchLocation = [touch locationInView:self.scrollView];
for (UIView *s in self.scrollView.subviews) {
if (CGRectContainsPoint(s.frame, touchLocation)) {
enableGestureRecognizer = NO;
}
}
}
return enableGestureRecognizer;
}
Seemed like a good idea, but it looks like I can't make my VC be the delegate.
Just tried setting scrollEnabled to NO on the scroll view. That successfully disabled scrolling, but it did not fix the problem. Views still occasionally do not respond to gestures. Thinking that perhaps some bug caused the gesture recognizer to fall off the object, I asked the debugger to display the gesture recognizers for the problematic views. They were still there. I'm stumped.
UPDATE: New information. I finally realized that the subviews that aren't responding are the ones on the right side of the screen. After carefully testing, it seems that this happens only in landscape orientation and only when the finger location is to the right of the right edge in portraite, i.e. 320 points. Apparently, something is not being handled property when rotating to landscape. Everything appears normal, but the gestures aren't being recognized.
Just for grins, I decided to display the frames and bounds and content area in the method viewDidLayoutSubviews. What I get is:
self.view.frame is {{0, 0}, {480, 320}}
self.view.bounds is {{0, 0}, {480, 320}}
self.scrollView.frame is {{0, 0}, {480, 320}}
self.scrollView.bounds is {{0, 0}, {480, 320}}
self.scrollView.contentSize is {480, 320}
I seem to have missed something. What else needs to be set when rotating?
use requireGestureRecognizerToFail: method.
you want your scroll view pan gesture (scrollViewGesture) to be failed when one of the gestures happen on its subView.
So, when you add pan gesture to your subView (subViewGesture), set below property as
scrollViewGesture.requireGestureRecognizerToFail =subViewGesture;
I found the solution. I'd forgotten that the subviews are not placed directly into the scroll view. There is a view originally occupying the bounds of scrollview onto which the subviews are placed. The hierarchy is like this:
self.view
scroll view
UIView (fills whole scroll view)
subview1
subview2
subviewn
In my code to handle rotation, I was not resizing the UIView into which the subviews are placed. Correcting this issue solved the problem.
I'd originally tried placing the subviews without their UIView superview in between them and the scroll view, but it didn't work for some reason. Adding this extra layer solved that problem, but I forgot to handle the resizing when rotating.
So I guess the gesture recognizers did not respond because although they were visible, they were outside the bounds of their superview.
I'm making this answer a community wiki because I haven't completely worked out this solution yet. The main thing is to take advantage of this from the documentation:
Subclasses can override the
touchesShouldBegin:withEvent:inContentView:, pagingEnabled, and
touchesShouldCancelInContentView: methods (which are called by the
scroll view) to affect how the scroll view handles scrolling
gestures.
One solution: instead of playing around with gesture recognizers, just disable all of them and use touchesBegan, touchesMove and touchesEnded directly. It might be a bit of work, but pretty sure it will work exactly the way you want.
You need to disable user interaction on the subviews, disable scrolling on the scrollview, and modify the scrollview's contentOffset directly.

How to conditionally "pass through" a gesture from a UIButton to a UIScrollView?

I have a long, horizontal, narrow UIScrollView containing several buttons side-by-side. When a user drags a button vertically, a specific method is triggered (via a UIPanGestureRecognizer), and the scrollview doesn't scroll (even if his/her drag begins to go left or right). This is all good.
When a user drags horizontally anywhere on the scrollview, including directly on a button, the UIPanGestureRecognizer ignores the effect and the scrollview should scroll. It's the last effect I'm having trouble with: when the user horizontally drags directly on a button, the scrollview doesn't scroll. How can I "pass through" that horizontal gesture to the scrollview?
Thanks for reading!
Add this method to your buttons' UIPanGestureRecognizer's delegate:
- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldRecognizeSimultaneouslyWithGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer
{
return (otherGestureRecognizer == _scrollView.panGestureRecognizer);
}
On UIScrollView you can override
- (BOOL)touchesShouldCancelInContentView:(UIView *)view
Subclass UIScrollView (or UITableView) and return YES. It will cancel any touch from subviews when scrolling starts.
Give a thought to UILongPressGestureRecognizer. This might help you: Combine longpress gesture and drag gesture together

iOS: Cancel UIScrollView touches when using 2 fingers

I have written a UIScrollView subclass that I am using to scroll a series of UITableViews. See the following diagram:
As you can see I have several vertically scrolling UITableViews, that are being scrolled horizontally inside a parent UIScrollView. This all works fine. However the application has a number of global gestures. For example, if I swipe in a given direction with 2 fingers, I do a UIView transition to another part of the app. but if I do the gesture on top of the scroll view and/or its child table views, they naturally scroll their content. This doesn't look good and causes some layout issues.
What I would like to figure out is how to disable all scrolling, on both the UIScrollView and its child UITableViews, when a user touches anywhere with two fingers, and only with two fingers. I've tried variations of overriding touchesBegan, touchesEnded, touchesShouldCancel etc... but I can't get it quite right. Any help is much appreciated.
Here is my gesture handling code:
UISwipeGestureRecognizer *twoFingerSwipeUp = [[UISwipeGestureRecognizer alloc] initWithTarget:self action:#selector(handleTwoFingerSwipe:)];
[twoFingerSwipeUp setNumberOfTouchesRequired:2];
[twoFingerSwipeUp setDirection:UISwipeGestureRecognizerDirectionUp];
[twoFingerSwipeUp setDelegate:self];
// 'self' is the superview of the UIScrollView, which is a UIView.
[self addGestureRecognizer:twoFingerSwipeUp];
[twoFingerSwipeUp release];
// ... repeat the above code for up, down, left, right gestures ...
- (void)handleTwoFingerSwipe:(UISwipeGestureRecognizer*)swipeGesture {
switch ([swipeGesture direction]) {
case UISwipeGestureRecognizerDirectionUp:
[self changeToView:viewAbove];
break;
case UISwipeGestureRecognizerDirectionDown:
[self changeToView:viewBelow];
break;
case UISwipeGestureRecognizerDirectionRight:
[self changeToView:viewToTheRight];
break;
case UISwipeGestureRecognizerDirectionLeft:
[self changeToView:viewToTheLeft];
break;
}
}
Try setting panGestureRecognizer.maximumNumberOfTouches = 1 on all scroll and table views (iOS 5 only).
If you're using a swipe recogniser for the two-finger swipe, require the recognisers of the scroll view (including the table views — they're scroll view as well) to fail when the two-finger recogniser recognises its gesture.
[[scrollView panGestureRecognizer] requireGestureRecognizerToFail: twoFingerRecogniser];
Iterate the above code for every scroll view and table view.
(P.S.: "recogniser" is British English, not a spelling err.)
Hope that helps. :-)
Write this code:
scrollView.minimumZoomScale=1.0;scrollView.maximumZoomScale=1.0;
scrollView.delegate self];
And Here is scrollViewDelegate Method:-
-(UIView*)viewForZoomingInScrollView:(UIScrollView *)aScrollView{
return aScrollView;}
One thing that you should be doing is to check that the gesture has finished before acting upon it:
if (swipeGesture.state == UIGestureRecognizerStateEnded) {
// Do your think
}
I've known odd things to happen otherwise.
Just disable user interaction in the parent scroll view. You need a UIWindow subclass and override -sendEvent: method because this gets called BEFORE any gesture recognizer. There, if you detect two touches, send a notification. Let the scroll view listen to it and disable user interaction if it occurs. And if touches ended, let it re-enable user interaction.

Detecting swipe gestures on UITableViewCell inside UIScrollView

I am hoping someone will be able to help me with a problem that is doing my head in at the moment!
Given the following view hierarchy
I want to be able to detect swipe gestures on my custom UITableViewCell.
I have subclassed the UIScrollView and have a hitTest:withEvent: method that checks whether I am touching the tableview cell (or its content) or not, in which case I set the following scroll view properties:
- (UIView*)hitTest:(CGPoint)point withEvent:(UIEvent *)event
{
UIView* result = [super hitTest:point withEvent:event];
if ([result.superview isKindOfClass:[UITableViewCell class]] || [result.superview tag] == SUBVIEW_TAG)
{
self.canCancelContentTouches = NO;
self.delaysContentTouches = YES;
} else {
self.canCancelContentTouches = YES;
self.delaysContentTouches = NO;
}
return result;
}
I have also implemented:
- (BOOL)touchesShouldCancelInContentView:(UIView *)view
{
if (view.tag == SUBVIEW_TAG || [[view superview] isKindOfClass:[UITableViewCell class]])
return NO;
return YES;
}
And am returning NO in case the view being touched is the table view cell.
These methods are all getting called and performing their actions as expected, but I am still unable to stop the UIScrollView from "hogging" the swipe gesture.
The interesting thing is that if I include the UIView that contains the tableview and cell on both of the methods above (the one with SUBVIEW_TAG) it works perfectly so I am guessing it must be something to do with the fact that UITableView inherits from UIScrollView.
My main goal is to be able to swipe on the cell to reveal more options for the cell. A horizontal swipe anywhere else on that view would be captured by the scroll view and shift the content horizontally as per its normal behaviour.
Any ideas would be very much appreciated!
Thanks!
Rog
I had a similar problem with a swipe detect for a component inside a scrollview and I was able to resolve it with
[scrollView.panGestureRecognizer requireGestureRecognizerToFail:swipeGesture]
Where scrollView is the scroll view object that acts like container and swipeGesture is the component swipe gesture object inside scrollview.
So, you can define a swipe for the cell object like this (for right swipe in the example, custom it as you want)
UISwipeGestureRecognizer* rightSwipeRecognizer = [[UISwipeGestureRecognizer alloc] initWithTarget:self action:#selector(yourMethod)];
[rightSwipeRecognizer setDirection:UISwipeGestureRecognizerDirectionLeft];
[cell addGestureRecognizer:rightSwipeRecognizer];
and then do
[scrollView.panGestureRecognizer requireGestureRecognizerToFail:rightSwipeRecognizer]
The documentation of requireGestureRecognizerToFail says:
This method creates a relationship with another gesture recognizer
that delays the receiver’s transition out of
UIGestureRecognizerStatePossible. The state that the receiver
transitions to depends on what happens with otherGestureRecognizer:
If otherGestureRecognizer transitions to
UIGestureRecognizerStateFailed, the receiver transitions to its normal
next state.
if otherGestureRecognizer transitions to
UIGestureRecognizerStateRecognized or UIGestureRecognizerStateBegan,
the receiver transitions to UIGestureRecognizerStateFailed.
An example where this method might be called is when you want a
single-tap gesture require that a double-tap gesture fail.
Availability Available in iOS 3.2 and later.
Hope helps!
The solution is pretty simple. All you need to do is add UIScrollView inside you UITableViewCell. It will prevent "hogging" effect during swipe gesture.

Resources