Intercept UIControlEventTouchUpInside in hitTest:withEvent: - ios

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.

Related

UIControl tracking touch that started on a different UIControl

I am making a custom iOS keyboard and have a UIControl subclass to represent my button. I am trying to get the same behaviour as the normal iOS keyboard:
User begins touch on one button
User drags over other buttons (need to detect this so they can highlight/dehighlight accordingly)
Register the actual keyboard "press" when the user lifts their finger; that is, the touch ends
I am testing using the touch tracking methods like this:
- (BOOL)beginTrackingWithTouch:(UITouch *)touch withEvent:(UIEvent *)event
{
[super beginTrackingWithTouch:touch withEvent:event];
NSLog(#"Begin for %#", [self label]);
return YES;
}
- (BOOL)continueTrackingWithTouch:(UITouch *)touch withEvent:(UIEvent *)event
{
[super continueTrackingWithTouch:touch withEvent:event];
NSLog(#"Continue for %#", [self label]);
return YES;
}
- (void)endTrackingWithTouch:(UITouch *)touch withEvent:(UIEvent *)event
{
[super endTrackingWithTouch:touch withEvent:event];
NSLog(#"End for %#", [self label]);
}
These methods are all called, except they are only ever called on the UIControl where the touch began.
What is the best way to recognise touches coming and going across all my buttons? Do I have to do it all via the parent view?
I'll select a better answer if offered... but in case anybody finds this by search, I've managed to get what I need this way:
Set userInteractionEnabled to NO for the button class (UIControl subclass)
Don't override any touch methods in the button class
Implement touchesBegan:withEvent:, touchesMoved:withEvent: and touchesEnded:withEvent: in the view controller
On each event, extract the location from the UITouch object
Iterate over all of the button subviews and find the one containing the touch location:
- (MyButton *)buttonForTouch:(UITouch *)touch
{
CGPoint windowLocation = [touch locationInView:keyboardView];
for (MyButton *button in buttons) {
if (CGRectContainsPoint([button frame], windowLocation)) {
return button;
}
}
return nil;
}
Having determined which button the user is interacting with, make the view controller send messages to the relevant buttons to adjust their appearance
If appropriate, keep a reference to the UITouch instance in touchesBegan:withEvent: so you can be sure that you're tracking the same one in the other methods
I think that you should have a single big UIControl which has different subviews (like UIButton) but tracks touches by itself like you did already but finds out which subview to highlight depending on the touch position.

Passthrough touches of 2 UIScrollViews

I have 2 descendants of UIScrollView
I have a UITableView which displays data
and i have a UICollectionView added above the UITableView
view
| - UITableView
| - UICollectionView
The UITableView can only scroll vertically and the UICollectionView can only scroll horizontally. I can only scroll my tableview where the collectionview isn't overlapping (which is off course expected behaviour) but i need to make it so that i can scroll my tableview even if i swipe vertically on my collectionview.
I cannot simply add the collectionview as a subview of the tableview because of other reasons (which i know, would make this work)
Is there any other possibility to let de touches from the collectionview passthrough to the tableview?
You can try to create a subclass of UICollectionView and add this code to your CustomCollectionView's .m file.
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event
{
UIView *hitView = [super hitTest:point withEvent:event];
if (hitView == self) {
return nil;
} else {
return hitView;
}
}
As I understood, you want touches to be intercepted by UITableView as well as UICollectionView?
I think You can try resending touch events from your UICollectionView to UITableView.
(manually calling touchesBegin, touchesMoved, touchesEnded, etc.)
Maybe overriding touchesBegan, touchesMoved, touchesEnded methods will work for your case.
You can try overriding UICollectionView with your subclass (with property set to your UITableView instance) and implementing touch handling methods with something like this:
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
[super touchesBegan:touches withEvent:event];
if (CGRectContainsPoint(self.tableView.frame, [touch locationInView:self.tableView.superview]) {
[self.tableView touchesBegan:touches withEvent:event];
}
}
- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event {
[super touchesMoved:touches withEvent:event];
[self.tableView touchesMoved:touches withEvent:event];
}
- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event {
[super touchesEnded:touches withEvent:event];
[self.tableView touchesEnded:touches withEvent:event];
}
- (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event {
[super touchesCancelled:touches withEvent:event];
[self.tableView touchesCancelled:touches withEvent:event];
}
Hope it will help, however I'm not 100% sure about it.
I've found this article also, maybe it will be useful
http://atastypixel.com/blog/a-trick-for-capturing-all-touch-input-for-the-duration-of-a-touch/
You can add pan gesture recognizer with direction vertical on collectionview. On the vertical pan event, you can change the content offset of your table view to scroll it.

Prevent touch events on subviews of scrolling UIScrollView in iOS7

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.

UIScrollView inside a UITableViewCell - No didSelect call

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

How to determine if UIView is currently being touched?

Is there a way I can query a UIView to determine if it currently being touched? The reason I ask is because I am using "touchedBegan", "touchesEnded", and "touchesMoved" to keep a UIView under a user's finger. However, if the user moves his/her finger really fast and manages to "escape" the window I want to remove that window.
I was thinking I could use a timer to periodically test each view to determine if it is currently being touched, if it is not, I will remove it. An "IsTouchedProperty" would be perfect.
Any thoughts?
I had a similar problem and solved it by using the tracking property. From the documentation:
A Boolean value that indicates whether the receiver is currently
tracking touches related to an event. (read-only)
This is a method on UIControl, but aren't you trying to create one?
You can also hitTest views. Check the apple docs
Since UIView doesn't inherit from UIControl you would need to subclass UIView and roll your own isTouching property using touch events. Something like:
// MyView.h
#interface MyView : UIView
#property (nonatomic, assign) BOOL isTouching;
#end
// MyView.m
...
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
[super touchesBegan:touches withEvent:event];
self.isTouching = YES;
}
- (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event {
[super touchesCancelled:touches withEvent:event];
self.isTouching = NO;
}
- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event {
[super touchesEnded:touches withEvent:event];
self.isTouching = NO;
}

Resources