I have a UIControl inside a UIScrollView. In my UIControl's init, I rig up some touch event handlers, e.g.
[self addTarget:_delegate
action:#selector(touchedDown) forControlEvents:UIControlEventTouchDown];
iOS6 and iOS7 behave differently when I do the following:
Swipe the UIScrollView to start scrolling
Tap the UIScrollView to stop scrolling
In iOS6, my app continues to behave as intended: the tap at step #2 does not call touchedDown -- the UIScrollView swallows the touch event as it immediately stops scrolling.
But in iOS7, the UIScrollView stops scrolling as expected, while touchedDown is still called.
Was there a documented API change? I'd like my app to behave identically to iOS6 in iOS7.
workaround for iOS 7
#interface UIScrollViewFixed : UIScrollView
#end
#implementation UIScrollViewFixed
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
if (self.isDragging || self.isDecelerating) {
return self;
}
return [super hitTest:point withEvent:event];
}
#end
Just replace the event type
UIControlEventTouchDown must be UIControlEventTouchUpInside
Not very elegant, but in the absence of any better ideas, here's what's working for me now:
On the UIScrollView, set canCancelContentTouches to YES and delaysContentTouches to NO.
In the UIScrollViewDelegate, toggle the UIScrollView's subview's userInteractionEnabled property when the UIScrollView scrolls:
- (void)scrollViewWillBeginDragging:(UIScrollView *)scrollView
{
[_contentView setUserInteractionEnabled:NO];
}
- (void)scrollViewDidEndDragging:(UIScrollView *)scrollView willDecelerate:(BOOL)decelerate
{
if (!decelerate) {
[_contentView setUserInteractionEnabled:YES];
}
}
- (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView
{
[_contentView setUserInteractionEnabled:YES];
}
Subclass the UIScrollView and implement:
- (BOOL)touchesShouldCancelInContentView:(UIView *)view
{
return YES;
}
Subclass the UIControl and implement touchesCancelled:withEvent to reverse whatever the UIControlEventTouchDown handler does:
- (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event
{
//custom logic
}
Same here with UIButtons on a UIScrollView. This is my solution for now.
Instead of using the contents UIControlEventTouchDown event:
[button addTarget:_delegate
action:#selector(touchedDown) forControlEvents:UIControlEventTouchDown];
I implemented the UIResponder touchesEnded method in my content UIViewController:
- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event{
// my scroll content touch logic
}
If the user touches the content and starts dragging the touchesEnded handler will not be called. The UIResponder touchesCanceled method will.
If the user does not drag the UIscrollview the touchesEnded handler is fired, which can be used for touch logic.
Related
I have subclassed UICollectionView as CoolGridCollectionView and I have overrides the touchesBegan: and touchesEnded: methods. But those are not getting called, when I tap the cell. And that cells are having single and double tap gesture recognisers. I need the position, where the tap has happened. I can get the position by using covertPoint: something else. But Why the touches methods are not called? Does iOS handles touches of UICollectionView specifically.
I’m not 100% sure, but you can try hitTest:withEvent: method instead of -touchesBegan:withEvent: method. Hope this will helpful to you.
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
touchedView = [super hitTest:point withEvent:event];
NSSet* touches = [event allTouches];
// handle touches if you need
return touchedView;
}
I've tried subclassing UICollectionView and overriding touchesBegan:withEvent: and hitTest:WithEvent:, and both of those methods trigger when I touch a cell. However, if I touch the space between the cells, nothing happens at all. Here's what I've created:
#interface WSImageGalleryCollectionView : UICollectionView
#end
..and..
#implementation WSImageGalleryCollectionView
- (void) touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
NSLog(#"Touches began");
[super touchesBegan:touches withEvent:event];
}
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
NSLog(#"Hit test reached");
return [super hitTest:point withEvent:event];
}
#end
Note: gesture recognizers seem to have the exact same issue, which is why I tried going lower-level with touchesBegan.
You just need to set a view as background view that you can then add a gesture recognizer to like:
collectionView.backgroundView = [[UIView alloc] init];
[collectionView.backgroundView addGestureRecognizer:[[UITapGestureRecognizer alloc] initWithTarget:self action:#selector(tapOnBackgroundRecognized)]];
You must have got something else that is preventing touchesBegan from firing.
Subclassing UICollectionView and using that subclass inside of a UICollectionViewController will allow you to detect in-between cells taps in touchesBegan.
I have just tried it out by modifying Apple's CollectionView-simple sample.
In my case, the problem was that you can click on the background whenever it is "clear" colored. Otherwise simply setting a background color makes the background clickable. I don't quite know why this is, but the simple fix was to just give it a background color.
Edit: Actually, this may have had something to do with setting the "opaque" flag.
I am having UIView with touches begin/moved/ended methods.I have added UIScrollView as a subview for the UIView.Now I am not receiving any touch events in those touch corresponding methods after adding UIScrollView as a subview.I tried setting the UIScrollView properties canCancelContentTouches, delaysContentTouches to NO.But still it is not working.
You can override UIView methods:
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event;
- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event;
Determine if the touch is within the bounds of subview (scrollview) and pass them to your parent view
return [super hitTest:point withEvent:event];
and
return [super pointInside:point withEvent:event];
I have a tableviewCell, where the user can scroll horizontally. Since the scrollView covers nearly the whole cell, the tableView method didSelectRow gets not called if the user clicks the cell.
So I thought, I could pass the touch event of the UIScrollView to the cell, but still the didSelectRow doesnt gets called.
I subclassed UIScrollView to pass the touch event only, if the touch was not a drag:
- (void) touchesEnded: (NSSet *) touches withEvent: (UIEvent *) event
{
NSLog(#"touch scroll");
// If not dragging, send event to next responder
if (!self.dragging)
[self.superview touchesEnded: touches withEvent:event];
else
[super touchesEnded: touches withEvent: event];
}
Any ideas on how to pass the click to the table, to get the delegate-methods called and keep the scrolling inside the scrollview?
You can actually do this without subclassing UIScrollView. Whether you have a custom cell, or are setting properties in cellForRowAtIndexPath in the UITableView, you can do the following:
[cell.contentView addSubview:yourScrollView];
yourScrollView.userInteractionEnabled = NO;
[cell.contentView addGestureRecognizer:yourScrollView.panGestureRecognizer];
The reason you can do this is because scrollView has its own panGestureRecognizer that's accessible to the programmer. So, just adding it to the cell's view will trigger the scrollview's gesture delegates.
The only drawback of this approach is that subviews of the scroll view are unable to receive any touch input. If you need this you will have to chose a different approach.
I just encountered the same problem.
In your subclass make sure to include the full set of methods:
-(void) touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event {
if (!self.dragging)
[self.superview touchesCancelled: touches withEvent:event];
else
[super touchesCancelled: touches withEvent: event];
}
-(void) touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event {
if (!self.dragging)
[self.superview touchesMoved: touches withEvent:event];
else
[super touchesMoved: touches withEvent: event];
}
-(void) touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
if (!self.dragging)
[self.superview touchesBegan: touches withEvent:event];
else
[super touchesBegan: touches withEvent: event];
}
-(void) touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event {
if (!self.dragging)
[self.superview touchesEnded: touches withEvent:event];
else
[super touchesEnded: touches withEvent: event];
}
The selected answer is correct, but I updated the code based on a bug I was getting.
- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event {
if (self.dragging) {
[super touchesMoved:touches withEvent:event];
} else {
if ([self.delegate isKindOfClass:[UITableViewCell class]]) {
[(UITableViewCell *)self.delegate touchesCancelled:touches withEvent:event];
}
[self.superview touchesMoved:touches withEvent:event];
}
}
If your self.delegate is not the UITableViewCell, than replace that property with a property to your cell.
The cell needs to retrieve the cancel touch event during movement to prevent the undesired results. It can be easily reproducible as follows.
Highlight the cell (assuming the scroll view is over the whole cell, if not highlight the scroll view)
While the cell is highlighted, drag the table view
Select any other cell and now the previously highlighted cell will retrieve the didSelectCell state
Another point to mention is that order matters! If the self.delegate is not called before the self.superview then the highlighted state wont happen.
Swift 3
scrollView.isUserInteractionEnabled = false
contentView.addGestureRecognizer(scrollView.panGestureRecognizer)
try set this
_scrollView.canCancelContentTouches = NO
also, it is bad to partially forward touch events
I have followed this great tutorial and I finally managed to implement a 3 independent rows scrollable interface.
I am left with a problem though, as the key of that tutorial is the use of method:
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event
{
NSLog(#"in hitTest");
if ([self pointInside:point withEvent:event]) {
return _scrollView;
}
return nil;
}
in order to handle the scrolling even when outside the scrollview area.
In fact my rows are filled with UIButtons and their TouchUpInside events got mixed up with hit events. Is there a way to make this method recognize those events and reject them, letting them propagate to legitimate delegate?
You should probably implement the -hitTest:withEvent: method as follows:
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
UIView *superView = [super hitTest:point withEvent:event];
if (superView == self)
return _scrollView;
return superView;
}
This will allow interaction within subviews of the UIScrollView.