UITapGestureRecognizer does not respond to subview area outside parent view - ios

I have a UIView called view1. view1 has a subview called subview. I added UITapGestureRecognizer to subview as follow:
UITapGestureRecognizer *recognizer = [[UITapGestureRecognizer alloc]initWithTarget:self action:#selector(handleTap:)];
[subview addGestureRecognizer:recognizer];
If I tapped an area overlapped between subview and view1 then the handleTap method got called. But if I tapped an area on the subview that was outside view1, then handleTap never got called. Is this behavior right? If not, any suggestion to what should I check for?
btw: The UIPanGestureRecognizer works fine. It does not exhibit the behavior mentioned above.

That is the default behaviour of UIView, the subview should be inside parent view bounds. If you want something different is better you create a custom subclass of the top view and override (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event

You need to customize the parent view and change the way it handles touches. See this question for more details.

I've found the answers talking about overriding pointInside:withEvent: to be lacking detail on explanation or implementation. In the original question, when the user taps in the black, unlabeled area/view (we'll call it view2), the event framework will only trigger hitTest:withEvent: for the main window down through the view2 (and its immediate subviews), and will never hit it for view1 because the point tested for in pointInside:point is outside of the bounds of view1's frame. In order to get subview1 to register the gesture, you should override view2's implementation of hitTest:withEvent to include a check for subview's pointInside:point
//This presumes view2 has a reference to view1 (since they're nested in the example).
//In scenarios where you don't have access, you'd need to implement this
//in a higher level in the view hierachy
//In view2
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
let ptRelativeToSubviewBounds = convert(point, to: view1.subview)
if view1.subview.point(inside:ptRelativeToSubviewBounds, with:event){
return view1.subview
}
else{
return super.hitTest(point, with: event)
}

Related

How to disable didSelectRowAtIndexPath on UIView Tap inside custom TableViewCell?

I have a UITableView with custom TableViewCell inside that custom UITableViewCell there is a UIView(myView). I want to disable didSelectRowAtIndexPath when I tap on myView,didSelectRowAtIndexPathshould not be called when I tap on myView.
it's not same as this question because in that question it's not clear what the user want to achieve and also answer is different.
You can override the pointInside of UITableViewCell, try this:
// MyCell.m
- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event {
BOOL pointInside = [super pointInside:point withEvent:event];
if (pointInside && ![self.myView pointInside:[self convertPoint:point toView:self.myView] withEvent:event]) {
return YES;
}
return NO;
}
didSelectRowAtIndexPathshould not be called when u tap on myView
Accodding to the Responder Chain say
iOS uses hit-testing to find the view that is under a touch. Hit-testing involves checking whether a touch is within the bounds of any relevant view objects. If it is, it recursively checks all of that view’s subviews. The lowest view in the view hierarchy that contains the touch point becomes the hit-test view. After iOS determines the hit-test view, it passes the touch event to that view for handling.
In the image blow :
(Figure 2-1 Hit-testing returns the subview that was touched)
(source: apple.com)
To illustrate, suppose that the user touches view E in Figure 2-1. iOS finds the hit-test view by checking the subviews in this order:
The touch is within the bounds of view A, so it checks subviews B
and C.
The touch is not within the bounds of view B, but it’s within the
bounds of view C, so it checks subviews D and E.
The touch is not within the bounds of view D, but it’s within the
bounds of view E. View E is the lowest view in the view hierarchy
that contains the touch, so it becomes the hit-test view.
As most app do,there is no need to disable the execution of didSelectRowAtIndexPath function.What you can do here ,is to add a UIButton or add a UITapGesture on your myView and perform the desired task,when you touch in the area of the UITapGesture, didSelectRowAtIndexPath will not execute.
Just like this:

UIScrollView with touch interceptor for contentSize to control subviews

assume I've got the following view hierarchy;
Those views are actually ShinobiCharts, which are subclasses of UIView. View1 acts as a main chart that can be touched by the user (pinch, pan, long press and so on). This view in turn controls the other views (View2, View3 and so on) with regards to specific, touch dependent properties (the user pans the View1, so views 2 and 3... have to act accordingly and pan as well).
However, once the user scrolls the UIScrollView, View1 may disappear from the screen, leaving only views 2, 3 etc. visible, which don't have the corresponding gesture recognizers, so the user can't interact with the charts any more, bad user experience.
I sure could add recognizers to these additional charts as well, but during pinch, that would mean that both fingers would have to be located within a single view, the user can't just touch where he wants in order to pinch and zoom, again, bad user experience.
To cut things short, I need some sort of touch interceptor that covers the entire content area of the UIScrollView so that when a user pinches or pans, the corresponding touches will be forwarded to the main chart (View1) which in turn can update the other subviews.
Vertical scrolling the UIScrollView should be possible at all times.
First experiment:
I've tried adding a transparent UIView to the ViewController that covers the UIScrollView.
However, even with a direct reference to View1, the touches wouldn't get forwarded.
-(void) touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
[self.chartReference touchesBegan:touches withEvent:event];
}
I'm not so sure if this is the correct way to achieve this anyways.
Second experiment:
Disabling userInteraction on the chart subviews in UIScrollView and add my own pinch and pan gestures (in UIViewController) to the UIScrollView.
Issues;
1. The UIScrollView doesn't scroll any more.
2. How to forward these gestures to View1?
I'm afraid I can't provide more example code at this point since there isn't really much relevant code to show yet.
Edit:
Small note to experiment two;
The gesture recognisers have been added as follows;
UIPinchGestureRecognizer *pinchGestureRecognizer = [[UIPinchGestureRecognizer alloc] initWithTarget:self action:#selector(pinchGesture:)];
pinchGestureRecognizer.cancelsTouchesInView = NO;
[scrollView addGestureRecognizer:pinchGestureRecognizer];
Simultaneous gesture recognition on the UIViewController has been enabled;
- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldRecognizeSimultaneouslyWithGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer {
return YES;
}
Edit:
Problem 2 for experiment 2 has been solved. I didn't set the correct delegate for the pan/pinch gestures. D'oh!
What remains is how to forward the pinch/pan gestures.
You're close with placing a transparent view on top of the others. But, you need to override one more method.
You want to override hitTest:withEvent on the transparent view. This method is used while traversing the responder chain to see which view handles a touch in a particular area. If a view handles that touch, it returns Itself. If it wants to pass it on to the next view below it, it returns nil. If it knows another view handles that touch, it can return that view.
So, in your case, if the point is in the target area of your top transparent view, you return view1. View1's gesture recognizer should then be called.
Example:
InterceptorView is a transparent view which lies on top of the scrollView. TargetView is a view inside the scrollView, and has a TapGestureRecognizer attached to it.
class InterceptorView: UIView {
required init(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
#IBOutlet weak var targetView1: UIView?
override func hitTest(point: CGPoint, withEvent event: UIEvent?) -> UIView? {
print("[Interceptor] Testing point: \(point) ")
if self.pointInside(point, withEvent: event) {
println("Hit")
return targetView1
}
else {
println()
return nil;
}
}
}
--
class TargetView: UIView {
required init(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
#IBAction func handleGesture(gestureRecognizer: UIGestureRecognizer) {
let location = gestureRecognizer.locationInView(self)
print("[TargetView] Got gesture. Location \(location) ")
if (pointInside(location, withEvent: nil)) {
println("Inside");
}
else {
println("Outside");
}
}
}
Here's the project:
https://github.com/annabd351/GestureForwarding
(there's some other stuff in there too, but it works!)

How to allow sibling UIViews to handle different gestures?

I have a UIView which has two subviews, one is a UIScrollView and the other is a container view for a few other subviews. The container view is covering the scroll view completely.
Views that need to handle gestures:
UIScrollView - should handle the default pinch and pan gestures
Container view - none
Container view subviews - should handle tap gesture
Now in order for the tap gestures to be handled by the container view subviews I implemented pointInside:withEvent: for the container view. If it recognises the point is inside one of its subviews it returns YES. This works fine. The problem is that when I pinch or pan and my finger initially touches one of the container view subviews it doesn't work. When I pinch or pan on an empty area of the container view it works as it should.
Any suggestions how to make it work?
EDIT:
I've implemented hitTest:withEvent: for the main view and got the same behavior.
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event
{
UIView *hitTestView;
for (UIView *subview in [self.subviews reverseObjectEnumerator])
{
hitTestView = [subview hitTest:[self convertPoint:point
toView:subview]
withEvent:event];
if (hitTestView && ![hitTestView isKindOfClass:[ContainerView class]])
{
break;
}
}
return hitTestView;
}
On the bottom line the question here is how does one view only handles some gestures and passes on other gestures so an underlying view could handle them.
I've read quite a lot about the subject and tried different approaches but couldn't find a straightforward solution to what seems like a pretty common issue.
You don't actually need to handle pinch and pan gesturese on UIScrollView manually, it's going to happen automatically.
For handling container view subviews you can use UITapGestureRecognizer. For each view you need to handle tap use:
UITapGestureRecognizer* tapRecognizer = [[UITapGestureRecognizer alloc] initWithTarget:self action:#selector(handleTapFirstSubview:)];
[firstSubview addGestureRecognizer:tapRecognizer];
Handler method:
- (void)handleTapFirstSubview:(UITapGestureRecognizer *)tapRecogmizer
{
// handle tap here
}

Detect touch event on UIScrollView AND on UIView's components [which is placed inside UIScrollView]

I have UIViewController on my simple storyboard IPad project which contains UIScrollView which is placed over entire surface (1024 x 768). I have created 3 XIB files which are UIViews which my application loads on start in viewDidLoad and add them into UIScrollView. Each of these 3 XIB files contains only one UIButton.
This is hierarchy:
~ UIViewController (UIViewControllerClass is class for this
UIViewController)
~~ UIScrollView (contains 3 identical UIViews)
~~~ UIView (UIViewClass is File's Owner for this XIB file)
~~~~ UIButton
I would like that my UIViewControllerClass becomes aware of both: touch anywhere on UIScrollView component AND if UIScrollView is touched, if UIButton inside of UIView in UIScrollView is touched, to have information that exactly that button is touched.
I made IBAction inside UIViewClass for touch on UIButton inside UIView in UIScrollView and when I set User Interaction Enabled = YES on all elements (UIViewController, UIView and UIScrollView) this method is called as it should.
But at this point my UIViewControllerClass isn't aware that touch occurred inside UIScrollView on that UIButton. I made touch recognizer like this:
UITapGestureRecognizer *touch = [[UITapGestureRecognizer alloc] initWithTarget:self action:#selector(handleTouch)];
touch.numberOfTouchesRequired = 1;
and added it to UIScrollView component. In this way I am able to detect touch event on UIScrollView component in UIViewControllerClass, but touch event handler for UIButton in UIView which is inside UIScrollView isn't called anymore.
So, I need to have these two informations in UIViewControllerClass:
Touch on UIScrollView component was made
Touch on UIButton in UIView which is inside UIScrollView (if this button was touched) was made
I suppose that attaching touch event recognizer to entire UIScrollView component isn't solution, since it disables all touch event handlers I wrote inside UIViewClass.
I think solution is that somehow touches which are made on components in UIView inside UIScrollView should be sent up to UIViewControllerClass, but I didn't found a way to do this.
If anyone can help me, I'd be very grateful. Thanks in advance.
[edit #1: ANSWER by Zheng]
Tap gesture must have cancelsTouchesInView option set to NO!
For my case above, this line solves everything:
touch.cancelsTouchesInView = NO;
Many thanks to Zheng.
I don't know if this works for you or not, but I've given an answer about touch events for views inside scrollview here:
Dismissing the keyboard in a UIScrollView
The idea is to tell the scrollView not to swallow up all tap gestures within the scroll view area.
I'll paste the code here anyways, hopefully it fixes your problem:
UITapGestureRecognizer *tapGesture = [[UITapGestureRecognizer alloc] initWithTarget:self action:#selector(hideKeyboard)];
// prevents the scroll view from swallowing up the touch event of child buttons
tapGesture.cancelsTouchesInView = NO;
[pageScrollView addGestureRecognizer:tapGesture];
[tapGesture release];
...
// method to hide keyboard when user taps on a scrollview
-(void)hideKeyboard
{
[myTextFieldInScrollView resignFirstResponder];
}
You can subclass your UIScrollView and override the method - hitTest:withEvent: which is called by the system to determine which view will handle the event. Whenever it is called, you can assume that a touch event occurred inside the scroll view, and by calling the super implementation, you can get the view which would normally process the event.
you can capture any kind of gestures in the UIscrollView. Make sure you also handle some of the default properties as well like set cancelsTouchesInView property to false, it is true by default. Also give some tag nos to your sub views to distinguish in selectors. & also enable their User interaction to true.
let tap = UITapGestureRecognizer(target: self, action:
selector(didTapByUser(_:)))

touches methods not getting called on UIView placed inside a UIScrollView

I have a Custom Scroll View, subclassing UIScrollView. I have added a scroll view in my viewcontroller nib file and changed its class to CustomScrollView. Now, this custom scroll view (made from xib) is added as a subview on self.view.
In this scroll view, I have 3 text fields and 1 UIImageView(named signImageView) added from xib. On clicking UIImageView (added a TapGestureRecogniser), a UIView named signView is added on the custom scroll view. I want to allow User to sign on this view, So I have created a class Signature.m and .h, subclassing UIView and implemented the touches methods (touchesBegan, touchesMoved and touchesEnded) and initialised the signView as follows:
signView = [[Signature alloc]initWithFrame:signImageView.frame];
[customScrollView addSubview:signView];
But when I start signing on the signView, the view gets scrolled and hence the touches methods don't get called.
I have tried adding signView on self.view instead of custom scroll view, but in that case the view remains glued to a fixed position when I start scrolling. (Its frame remains fixed in this case)
Try setting canCancelContentTouches of the scrollView to NO and delaysContentTouches to YES.
EDIT:
I see that similiar question was answered here Drag & sweep with Cocoa on iPhone (the answer is exactly the same).
If the user tap-n-holds the signView (for about 0.3-0.5 seconds) then view's touchesBegan: method gets fired and all events from that moment on go to the signView until touchesEnded: is called.
If user quickly swipes trough the signView then UIScrollView takes over.
Since you already have UIView subclassed with touchesBegan: method implemented maybe you could somehow indicate to user that your app is prepared for him to sign ('green light' equivalent).
You could also use touchesEnded: to turn off this green light.
It might be better if you add signImageView as as subView of signView (instead of to customScrollView) and hide it when touchesBegan: is fired). You would add signView to customScrollview at the same place where you add signImageView in existing code instead.
With this you achieve that there is effectively only one subView on that place (for better touch-passing efficiency. And you could achieve that green light effect by un-hiding signImageView in touchesBegan:/touchesEnded:
If this app-behaviour (0.3-0.5s delay) is unacceptable then you'd also need to subclass UIScrollView. There Vignesh's method of overriding UIScrollView's touchesShouldBegin: could come to the rescue. There you could possibly detect if the touch accoured in signView and pass it to that view immediately.
When ever you add a scrollview in your view hierarchy it swallows all touches.Hence you are not getting the touches began. So to get the touches in your signon view you will have to pass the touches to signon view. This is how you do it.
We achieved this with a UIScrollView subclass that disables the pan gesture recogniser for a list of views that you provide.
class PanGestureSelectiveScrollView: UIScrollView {
var disablePanOnViews: [UIView]?
override func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool {
guard let disablePanOnViews = disablePanOnViews else {
return super.gestureRecognizerShouldBegin(gestureRecognizer)
}
let touchPoint = gestureRecognizer.location(in: self)
let isTouchingAnyDisablingView = disablePanOnViews.first { $0.frame.contains(touchPoint) } != nil
if gestureRecognizer === panGestureRecognizer && isTouchingAnyDisablingView {
return false
}
return true
}
}

Resources