Suppose I have a UIView named Cake. Cake has a gesture recognizer.
Now, suppose I have a UIButton named Bob.
I add Cake as a subview to Bob:
[Bob addSubview: Cake];
Now, Bob, the UIButton, no longer responds the control event touch up inside.
I want Cake to be able to handle the touch while Bob simultaneously handles the touch as well. Currently, Cake can handle the touch, but Bob lazily does nothing.
Things I have tried:
Setting cancelsTouchesInView of Cake's gesture recognizer to NO
Implementing the UIGestureRecognizerdelegate for Cake's gesture recognizer and always returning YES for the shouldRecognizeSimultaneouslyWithGestureRecognizer method
Subclassing UIGestureRecognizer and calling [self.view.nextResponder touchesSomething:touches withEvent:event]; in each of the touchesSomething (touchesBegan, touchesEnded, etc.) methods (I've also confirmed that the next responder IS IN FACT the UIButton that is supposed to handle the control events)
Not using a gesture recognizer and instead just using the touchesSomething methods in the UIView (Cake) + passing through the touchesSomething calls to all of super, self.superview, self.nextResponder and more.
Does anyone know a good way to make this work?
My suggestion is the following:
Make sure, that you set the UserInteraction of the subview to false! Now the action of the button should be called correctly.
Now add the event parameter to the action:
- (IBAction)pressButton:(UIButton *)sender forEvent:(UIEvent *)event
Inside the action check if the press was inside the subview
- (IBAction)pressButton:(UIButton *)sender forEvent:(UIEvent *)event
// get location
UITouch *touch = [[event allTouches] anyObject];
CGPoint location = [touch locationInView:touch.view];
// check position
if (CGRectContainsPoint(self.subview.frame, location) {
// call selector like the gesture recognizer here
}
}
What I'm trying to make, Cake, is a view that can be placed as a subview into any button without additional setup - its a decorative view of sorts. The gesture recognizer of Cake is there to make a small animation
Then you're going about this all wrong. Take away the gesture recognizer of Cake; you don't need it. You're trying to get Cake to respond to Bob being pressed. But that's easy; Bob's a button! The button already tells you everything that's happening — it's being highlighted etc. So all you need is a UIButton subclass that tells Cake when to do its animation.
Related
I'm trying to get a particular event to work.
The user should touch one button (A), this button would pop-up some UIView, in which there is another button (B).
If the user touchUpInside button A, the popup should disappear, and the Button (B) would not be clickable.
However, if the user clicks on button (A), and then drag his finger into the button (B) then this button (B) will be selected, but only fire if the user TouchUpInside this button (B)
I have tried the most obvious TouchDragEnter and TouchDragInside, but it does not do what I expected, you have to touchDownInside first for it to work. Since the TouchDownInside event has been done on the button (A), it can't be done on the button (B)
Have I missed something, or should I just go ahead a create a subClass of my own for this particular behaviour ?
To add some difficulty, the button (A) and button (B) are not on the same UIView.
If I was to go about this, I would ignore touchUpInside etc altogether and just focus on the drag event. In the touchesMoved method, keep checking where the touches are, and if they intersect the rect of the view you are concerned about.
Something like this : (apologies for incomplete example)
- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event
{
UITouch *touch = [touches anyObject];
CGPoint currentPoint = [touch locationInView:self];
if ( CGRectContainsPoint(imageView.bounds, locationInView) ) {
// Point lies inside the bounds...
I have just created a library called PopMenu that implements exactly the function you mentioned. The idea is it pops out buttons when you touchdown on the menu button and keep tracking the user's finger until the user touches up on the screen. It returns the button that the user ended his touch on.
I need to be able to detect immediate touch and get its position. (so didSelectRowAtIndexPath can't help us since it does not act immediately when scrolling up and down fast, you need to breathe in and select one by one)
Already tried everything I can think of. Touches began in each cell does not work because it suddenly behaves like didSelectRowIndexPath when implemented in custom cell class. Same result with TableViewController, the nature of touches began (you touch it, respond right away) just won't work.
* I'm not trying to TAP. Need to be able to get TOUCH (TapGesture does not respond when swiping very carefully/slowly but touches began always does) *
Not sure it's what you need, but you can create a TapGestureRecognizer.
You will likely run into conflicts with the UITableView's own gesture recognisers, but there are mechanisms to solve these which should hopefully let you achieve your desired behaviour (look up requireGestureRecognizerToFail and UIGestureRecognizerDelegate's gestureRecognizerShouldBegin and shouldRecognizeSimultaneouslyWithGestureRecognizer).
Try this.
In cellForIndexpath method.
UITapGestureRecognizer *singleFingerTap =
[[UITapGestureRecognizer alloc] initWithTarget:self
action:#selector(handleSingleTap:)];
singleFingerTap.delegate=self;
cell.contentView.tag=indexPath.row;
[cell.contentView addGestureRecognizer:singleFingerTap];
//The event handling method
- (void)handleSingleTap:(UITapGestureRecognizer *)recognizer {
NSLog(#"%ld",(long)recognizer.view.tag);
}
I think what you need to do, add UILongPressGestureRecognizer to tableview, so normal touch will scroll and long press will do whatever you want to. Set its minimumPressDuration like 0.2 so it won't take much time. Add action for UILongPressGestureRecognizer and in that method get location like:
CGPoint touchPointInView = [sender locationInView: self.view]; //location reespective to view
CGPoint touchPointInView1 = [sender locationInView: tableView]; //location respective to tableview
i am using 2 pods.
MMDrawerController 0.5.1& WYPopoverController 0.1.7
now i want to make a WYPopover on my MMView
some pics:
this is how the MMController looks like (Playground)
now i want to touch it anywhere and make it look like:
the error: if i want to open the left view from the 'MMController' sometimes i get this:
but it should look like (left MMControllerView)
i am using the 2 methods:
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
// show the popoverController
}
- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event {
[popController dismissPopoverAnimated:NO];
}
my question: why the touchesMoved isn't called continuous and if there is a better way?
I don't know anything about the "pods" you mention (CocoaPods?) However, the most straightforward way to distinguish between taps and drags is to use a pair of gesture recognizers, a tap gesture recognizer and a pan gesture recognizer, and set up the tap gesture recognizer so the pan gesture must fail before the tap is triggered (There is a "wait until another gesture recognizer fails" mechanism built into gesture recognizers.)
I suggest reading up on UIGestureRecognizer, and the specific classes UITapGestureRecognizer and UIPanGestureRecognizer
You can dismiss popover when your left menu is going to open,
i.e. you can do that in delegate method of your slide menu:
- menuWillOpen: or something like this.
I want both my UIScrollView and its subviews to receive all touch events inside the subview. Each can respond in its own way.
Alternatively, if tap gestures were forwarded to subviews, all would be well.
A lot of people are struggling in this general area. Here are a few of the many related questions:
How does UIScrollView steal touches from its subviews
How to steal touches from UIScrollView?
How to Cancel Scrolling in UIScrollView
Incidentally, if I override hitTest:withEvent: in the scroll view, I do see the touches as long as userInteractionEnabled is YES. But that doesn't really solve my problem, because:
1) At that point, I don't know if it's a tap or not.
2) Sometimes I need to set userInteractionEnabled to NO.
EDIT: To clarify, yes, I want to treat taps differently from pans. Taps should be handled by subviews. Pans can be handled by the scroll view in the usual way.
First, a disclaimer. If you set userInteractionEnabled to NO on the UIScrollView, no touch events will be passed to the subviews. So far as I'm aware, there's no way around that with one exception: intercept touch events on the superview of the UIScrollView, and specifically pass those events to the subviews of UIScrollView. To be honest, though, I don't know why you would want to do this. If you're wanting to disable specific UIScrollView functionality (like...well, scrolling) you can do that easily enough without disabling UserInteraction.
If I understand your question, you need tap events to be processed by the UIScrollView and passed to the subviews? In any case (whatever the gesture is), I think what you're looking for is the protocol method gestureRecognizer:shouldRecognizeSimultaneouslyWithGestureRecognizer: in the protocol UIGestureRecognizerDelegate. In your subviews, whatever gesture recognizers you have, set a delegate (probably whatever class is setting the UIGestureReconginzer in the first place) on the gesture recognizer. Override the above method and return YES. Now, this gesture will be recognized along with any other recognizers that might have 'stolen' the gesture (in your case, a tap). Using this method you can even fine tune your code to only send certain kinds of gestures to the subviews or send the gesture only in certain situations. It gives you a lot of control. Just be sure to read about the method, especially this part:
This method is called when recognition of a gesture by
either gestureRecognizer or otherGestureRecognizer would block the
other gesture recognizer from recognizing its gesture. Note that
returning YES is guaranteed to allow simultaneous recognition;
returning NO, on the other hand, is not guaranteed to prevent
simultaneous recognition because the other gesture recognizer's
delegate may return YES.
Of course, there's a caveat: This only applies to gesture recognizers. So you may still have problems if you're trying to use touchesBegan:, touchesEnded, etc to process the touches. You can, of course, use hitTest: to send raw touch events on to the subviews, but why? Why process the events using those methods in UIView, when you can attach a UIGestureRecognizer to a view and get all of that functionality for free? If you need touches processed in a way that no standard UIGestureRecognizer can provide, subclass UIGestureRecognizer and process the touches there. That way you get all the the functionality of a UIGestureRecognizer along with your own custom touch processing. I really think Apple intended for UIGestureRecognizer to replace most (if not all) of the custom touch processing code that developers use on UIView. It allows for code-reuse and it's a lot easier to deal with when mitigating what code processes what touch event.
I don't know if this can help you, but I had a similar problem, where I wanted the scrollview to handle double-tap, but forward single tap to subviews. Here is the code used in a CustomScrollView
- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event {
UITouch* touch = [touches anyObject];
// Coordinates
CGPoint point = [touch locationInView:[self.subviews objectAtIndex:0]];
// One tap, forward
if(touch.tapCount == 1){
// for each subview
for(UIView* overlayView in self.subviews){
// Forward to my subclasss only
if([overlayView isKindOfClass:[OverlayView class]]){
// translate coordinate
CGPoint newPoint = [touch locationInView:overlayView];
//NSLog(#"%#",NSStringFromCGPoint(newPoint));
BOOL isInside = [overlayView pointInside:newPoint withEvent:event];
//if subview is hit
if(isInside){
Forwarding
[overlayView touchesEnded:touches withEvent:event];
break;
}
}
}
}
// double tap : handle zoom
else if(touch.tapCount == 2){
if(self.zoomScale == self.maximumZoomScale){
[self setZoomScale:[self minimumZoomScale] animated:YES];
} else {
CGRect zoomRect = [self zoomRectForScrollView:self withScale:self.maximumZoomScale withCenter:point];
[self zoomToRect:zoomRect animated:YES];
}
[self setNeedsDisplay];
}
}
Of course, the effective code should be changed, but at this point you should have all the informations you need to decide if you have to forward the event. You might need to implement this in another method as touchesMoved:withEvent:.
Hope this can help.
I was having this same problem, but with a scrollview that was inside UIPageViewController, so it had to be handled slightly differently.
By changing the cancelsTouchesInView property to false for each recognizer on the UIScrollView I was able to receives touches to buttons inside the UIPageViewController.
I did so by adding this code into viewDidLoad:
guard let recognizers = self.pageViewController.view.subviews[0].gestureRecognizers else {
print("No gesture recognizers on scrollview.")
return
}
for recognizer in recognizers {
recognizer.cancelsTouchesInView = false
}
If what you need is to differ between a touch and a scroll then you can test if touches has been moved. If this is a tap then touchHasBeenMoved will not be called then you can assume this is a touch.
At this point you can set a boolean to indicate if a movnent accoured and set this Boolean as a condition in your other methods.
I am on the road but if that's what you need I will be able to explain better later.
A hackish way to achieve your objective - not 100% exact - is to subclass the UIWindow and override the - (void)sendEvent:(UIEvent *)event;
A quick example:
in SecondResponderWindow.h header
//SecondResponderWindow.h
#protocol SecondResponderWindowDelegate
- (void)userTouchBegan:(id)tapPoint onView:(UIView*)aView;
- (void)userTouchMoved:(id)tapPoint onView:(UIView*)aView;
- (void)userTouchEnded:(id)tapPoint onView:(UIView*)aView;
#end
#interface SecondResponderWindow : UIWindow
#property (nonatomic, retain) UIView *viewToObserve;
#property (nonatomic, assign) id <SecondResponderWindowDelegate> controllerThatObserves;
#end
in SecondResponderWindow.m
//SecondResponderWindow.m
- (void)forwardTouchBegan:(id)touch onView:(UIView*)aView {
[controllerThatObserves userTouchBegan:touch onView:aView];
}
- (void)forwardTouchMoved:(id)touch onView:(UIView*)aView {
[controllerThatObserves userTouchMoved:touch onView:aView];
}
- (void)forwardTouchEnded:(id)touch onView:(UIView*)aView {
[controllerThatObserves userTouchEnded:touch onView:aView];
}
- (void)sendEvent:(UIEvent *)event {
[super sendEvent:event];
if (viewToObserve == nil || controllerThatObserves == nil) return;
NSSet *touches = [event allTouches];
UITouch *touch = [touches anyObject];
if ([touch.view isDescendantOfView:viewToObserve] == NO) return;
CGPoint tapPoint = [touch locationInView:viewToObserve];
NSValue *pointValue = [NSValue valueWithCGPoint:tapPoint];
if (touch.phase == UITouchPhaseBegan)
[self forwardTouchBegan:pointValue onView:touch.view];
else if (touch.phase == UITouchPhaseMoved)
[self forwardTouchMoved:pointValue onView:touch.view];
else if (touch.phase == UITouchPhaseEnded)
[self forwardTouchEnded:pointValue onView:touch.view];
else if (touch.phase == UITouchPhaseCancelled)
[self forwardTouchEnded:pointValue onView:touch.view];
}
It's not 100% conforms to what your were expecting - because your second responder view does not handle the touch event natively via -touchDidBegin: or so, and has to implement the SecondResponderWindowDelegate. However this hack does allow you to handle touch events on additional responders.
This method is inspired by and extended from MITHIN KUMAR's TapDetectingWindow
I have a subclass of UIView on top of a UITableView. I am using the UITableView to display some data and, at the same time, I would like to overlay an animation that follows the finger (for instance, leaving a trail).
If I get it right, I need the touch events to be handled both by the UIView subclass and the UITableView. How can I do that?
Is it possible to have, ie, touchesMoved being triggered on the UIView subclass and then on UITableView?
Thank you so much for any help.
The way I have solved this problem is in a way that is not that clean, but it works. Please let me know if there's a better way to do this.
I have overridden hitTest for my custom UIView so that it directs touches to the UITableView underneath. Then in the UITableView I am handling the gestures through touchesBegan, touchesMoved, etc. There I am also calling touchesBegan on the UIView.
In this way touches are handled by two views.
The reason why I am not doing the other way around (having UIView's touchesBegan calling UITableView's touchesBegan) is that gestures recognizers on the UITableView would not work.
UIView subclass' hitTest
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event
{
// tview is the UITableView subclass instance
CGPoint tViewHit = [tView convertPoint:point fromView:self];
if ([tView pointInside:tViewHit withEvent:event]) return tView;
return [super hitTest:point withEvent:event];
}
UITableView subclass's touchesBegan
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
UITouch *touch = [touches anyObject];
CGPoint location = [touch locationInView:touch.view];
// ....
// view is the UIView's subclass instance
[view touchesBegan:touches withEvent:event];
}
No, you cann't do it implicity. Event Delivery chapter says
The window object uses hit-testing and the responder chain to find the
view to receive the touch event. In hit-testing, a window calls
hitTest:withEvent: on the top-most view of the view hierarchy; this
method proceeds by recursively calling pointInside:withEvent: on each
view in the view hierarchy that returns YES, proceeding down the
hierarchy until it finds the subview within whose bounds the touch
took place. That view becomes the hit-test view.
So, when window finds touched view it returns YES. Only one view can handle touches at the current moment.
But if you need to handle event for UITableView then handle it for UIView! You can convert touched point to required coordinates with – convertPoint, – convertRect functions, add subview to UITableView and move it depends on coordinate, and a lot of another things.
UITableView relays unhandled touch events to UIView. (Google "responder chain")
UITableView Documentation
So, you can handle your touch events in UIView only. So. In your UIView
touchesstart - do initialization stuff
touchesmove - draw tail on UIView (Use timers/delayedresponse to desable points so that it would look like a trail)
touchesend - do remaining stuff
Hope this helps.