I have a PageViewController which is initialized like this:
self.pageViewController = [[UIPageViewController alloc] initWithTransitionStyle:UIPageViewControllerTransitionStyleScroll
navigationOrientation:UIPageViewControllerNavigationOrientationHorizontal options:nil];
On one of the pages, there's a UISlider.
My problem is that when I have transitionstyle set to UIPageViewControllerTransitionStyleScroll, it takes 150-200 ms before beginTrackingWithTouch is invoked on the slider.
This behavior is not seen when I use UIPageViewControllerTransitionStylePageCurl, where the UISlider is selected instantly.
This means that unless the user waits a bit before dragging the slider (a video progress), the page will turn instead, which is far from ideal.
The Page curl animation does not meet the demands of the app, so any explanation or workaround is appreciated.
Since with UIPageViewControllerTransitionStyleScroll gesture recognizers isn't available, you can use this:
for (UIView *view in pageViewController.view.subviews) {
if ([view isKindOfClass:[UIScrollView class]]) {
UIScrollView *scrollView = (UIScrollView *)view;
scrollView.delaysContentTouches = NO;
}
}
I solved this issue by add a pan gesture on UISlider and set:
self.sliderGesture.cancelsTouchesInView = NO; // make touch always triggered
and implement delegate method like:
- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldBeRequiredToFailByGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer
{
return otherGestureRecognizer.view.superview == self.parentViewController.view;
}
- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldReceiveTouch:(UITouch *)touch {
// only receive touch in slider
CGPoint touchLocation = [touch locationInView:self.view];
return CGRectContainsPoint(self.slider.frame, touchLocation);
}
You can try to set the delegate of the page view controller gestures to the root view controller:
for (UIGestureRecognizer* gestureRecognizer in self.pageViewController.gestureRecognizers) {
gestureRecognizer.delegate = self;
}
And then prevent the touch of the gestures if it appears inside UISlider which is a subclass of UIControl:
- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldReceiveTouch:(UITouch *)touch {
return ([touch.view isKindOfClass:[UIControl class]] == NO);
}
What helped me was to add pan-gesture-recognizer to UIView which holds UISlider, so in the end I have
UIPageViewController->UIScrollView->...->MyView->UISlider
The 'MyView' thing had pan gesture registered to it which did nothing, but served just to NOT propagate events to scroll view.
Related
What I want to do is put a UIView on top of another UIView, and both of them are screen size. The top UIView includes lots cocos nodes and will respond when I touch them. But when I touch a place that has no cocos node, the bottom UIView should respond.
I don't know how to do this. My imagine is check if top uiView is handled touch, do nothing. other wise let the bottom UIView start respond. But I don't know how to check that. I only know how to check touch but it seems the UIView will also be touched when I touch some place it can't handle.
I think you not requires 2 views.
Just take one view only and add all your node to that view.
in .h
IBOutlet UIView *bgView;//your view
UITapGestureRecognizer *viewTapRecognizer;// view tap recognizer
in .m
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view, typically from a nib.
viewTapRecognizer = [[UITapGestureRecognizer alloc] initWithTarget:self action:#selector(handelGesture:)];
[bgView addGestureRecognizer:viewTapRecognizer];
for (UIView *subView in [bgView subviews]) {
UITapGestureRecognizer *nodeTapRecognizer = [[UITapGestureRecognizer alloc] initWithTarget:self action:#selector(handelGesture:)];
[subView addGestureRecognizer:nodeTapRecognizer];
}
}
- (void)handelGesture:(UITapGestureRecognizer*)sender {
if (sender == viewTapRecognizer) {
// your view is tapped
NSLog(#"........Tapped view..........");
}
else {
// your node is tapped
NSLog(#"........Tapped node..........");
}
}
Try this. it might work for you.
You can achieve it by 2 ways: using pointInside... method or hitTest... method.
If you will use pointInside... you can use only your views (the top view and the bottom view). You will subclass UIView with the top view and override pointInside... method. You will return YES if user's tapped a cocoa node else returns NO.
- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event
{
BOOL inside = [self didUserTapCocoaNode:point];
return inside;
}
If you want more complex logic, you should use third view, subclass of UIView (the container view), that will be contain the top view and the bottom view. Override method hitTest..., and return the bottom or the top view.
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event
{
UIView* result = nil;
if ([self touchedCocoaNode:point])
{
result = self.topView;
}
else
{
result = self.bottomView;
}
return result;
}
UPD
Example implementation of didUserTapCocoaNode with pseudo code:
- (BOOL)didUserTapCocoaNode:(CGPoint)pointInSelf
{
__block BOOL tappedSomeNode = NO;
[self.nodes enumerateObjectsUsingBlock:^(NodeType* obj, NSInteger idx, BOOL* stop){
CGPoint pointInNode = [obj convertPoint:pointInSelf fromView:self];
tappedSomeNode = [obj pointInside:pointInNode withEvent:nil];
*stop = tappedSomeNode;
}]
return tappedSomeNode;
}
I have a UIView with a UIPanGestureRecognizer attached to it.
I also have an object within the UIView that has multiple UISwipeGestureRecognizers.
The UIPanGestureRecognizer and the UISwipeGestureRecognizers associated with the object overlap.
Is there any way to make the UIPanGestureRecognizer totally ignore a certain area of the UIView or make the object's UISwipeGestureRecognizers take precedence and override the UIView's UIPanGestureRecognizer?
What you want is...
-(BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldRecognizeSimultaneouslyWithGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer
{
if(gestureRecognizer == myPanGesture) return NO;
return YES;
}
Or a similar usage of that delegate method. It is part of the UIGestureRecognizerDelegate protocol. This would allow you to not recognize the panning if you are swiping.
Solved this problem using this delegate method:
- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldReceiveTouch:(UITouch *)touch
{
if ([touch.view isKindOfClass:[UIButton class]] && gestureRecognizer == recognizer) return NO;
return YES;
}
Thanks for pointing me in the right direction #MikeS
Thanks a lot for the answers, which helped me with my issue.
I just want to share my solution, because it can be helpful:
-(BOOL) gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldReceiveTouch:(UITouch *)touch{
NSLog2(#"Gesture on Class %# tag %i", [touch.view class], touch.view.tag);////////
if (touch.view.tag == kTagToIgnoreGestures){
return NO;
}
return YES;
}
I defined a kTagToIgnoreGestures which is tag of views that should ignore gestures.
This way I can have 2 subviews in a view with UIGestureRecognizer, that only one of them will be effected by gestures.
Hope it helps. Shefy
I have a UITableView and UINavigationController, and I'd like to distinguish between two clicks: 1) normal click that selects a row and 2) a click that happens ANYWHERE else on the screen (other than the buttons on the UINavigationController). I wrote this code:
singleTap = [[UITapGestureRecognizer alloc]
initWithTarget:self action:#selector(singleTapHandler:)];
singleTap.numberOfTapsRequired = 1;
The problem with this however is that it overrides the normal clicks that select a row.
I assume you're putting the tap recognizer on either the UIWindow itself, or the window's sole subview. You need to give the tap recognizer a delegate, and that delegate needs to implement gestureRecognizer:shouldReceiveTouch:.
In that method, you want to return NO if the touch is in a button or if the touch is in a table view cell, and YES otherwise. You need to walk up the view hierarchy, starting with the view that the touch landed in, looking for either of those classes.
- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldReceiveTouch:(UITouch *)touch {
for (UIView *view = touch.view; view; view = view.superview) {
if ([view isKindOfClass:[UIButton class]])
return NO;
if ([view isKindOfClass:[UITableViewCell class]])
return NO;
}
return YES;
}
I have a custom UIView that I am displaying as a callout when the user clicks on a custom annotation on an MKMapView.
To achieve this I have subclassed MKAnnotationView and overloaded the -setSelected:selected animated: method as suggested in this answer. Basically, I am adding my custom view as a subview of my MKAnnotationView subclass.
The problem is that I can't interact with the callout, which contains a button and a scrollable webview, at all. What's more, if the callout hides an annotation and I press the callout at the approximate location of that hidden annotation, the callout will get dismissed and a new one will be shown.
// TPMapAnnotationView.m
#implementation TPMapAnnotationView
- (void)setSelected:(BOOL)selected animated:(BOOL)animated
{
[super setSelected:selected animated:animated];
if(selected)
{
TPMapAnnotation* anno = ((TPMapAnnotation*)self.annotation);
QuickInfoView* qi = [[QuickTabView alloc] initWithFrame:CGRectMake(0, 0, 440, 300)];
[qi displayDataForAnnotation:anno];
[self addSubview:qi];
// some animiation code that doesn't change things either way
}
else
{
[[self.subviews objectAtIndex:0] removeFromSuperview];
}
}
The code below creates the TPMapAnnotationView.
// this is in the view controller that contains the MKMapView
- (MKAnnotationView *) mapView:(MKMapView *) mapView viewForAnnotation:(id) annotation
{
if ([annotation isKindOfClass:[TPMapAnnotation class]])
{
TPMapAnnotationView *customAnnotationView = (TPMapAnnotationView *)[myMap dequeueReusableAnnotationViewWithIdentifier:#"TPAnn"];
if (customAnnotationView == nil)
{
customAnnotationView = [[TPMapAnnotationView alloc] initWithAnnotation:annotation
reuseIdentifier:#"TPAnn"];
}
[customAnnotationView setImage:annotationImage];
return customAnnotationView;
}
return nil; // blue radar circle for MKUserLocation class.
}
This is a well known problem. Anything you add on AnnotationView will not detect touches. There is good open source project for this problem. http://dev.tuyennguyen.ca/wp-content/uploads/2011/03/CustomMapAnnotationBlogPart1.zip, http://dev.tuyennguyen.ca/wp-content/uploads/2011/03/CustomMapAnnotationBlogPart21.zip
EDIT:
Yes. I also tried hard to add uibuttons to my own custom annotationView but then I stumbled upon this project and found that his custom annotationView is actually a annotation.
Anyway if you want to to change height of annotatioView then you can set
- (MKAnnotationView *) mapView:(MKMapView *)mapView viewForAnnotation:(id <MKAnnotation>) annotation{
calloutMapAnnotationView.contentHeight = height;
calloutMapAnnotationView.titleHeight = 25;
}
here, titleHeight is property added to CalloutMapAnnotationView which determines height of "gloss" in drawRectMethod
- (void)drawRect:(CGRect)rect {
glossRect.size.height = self.titleHeight;
}
if you are having any difficulty please let me know.
And also the link to original blogpost:
http://dev.tuyennguyen.ca/?p=298
I resolved this problem. Click anything in CalloutView,the map will not get touch.My calloutview is custom have tabbleview
1 - In file MapviewController.h you will add delegate : UIGestureRecognizerDelegate
2 - and in file MapViewController.m implement method - (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldReceiveTouch:(UITouch *)touch
-In my mapView when you click 1 time on Map it will go in this method 3 time. So I limit touch will action.the first touch will action.
- In myCalloutView have tabbleView, if tabbleView receive touch It will return false touch for Map, it will make your tabbleview can get touch.It same for your button.
Note : in NSlog hit test View : will have name of view item you want it have touch.
example my view : isEqualToString:#"UITableViewCellContentView"]
static int count=0;
- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldReceiveTouch:(UITouch *)touch{
NSLog(#"hit test view %#",[touch view]);
if(count >0 && count<=2)
{
count++;
count=count%2;
return FALSE;
}
count++;
if ([[[[touch view] class] description] isEqualToString:#"UITableViewCellContentView"]) {
return FALSE;
}
return TRUE;
}
I'm trying to figure out how this is done the right way. I've tried to depict the situation:
I'm adding a UITableView as a subview of a UIView. The UIView responds to a tap- and pinchGestureRecognizer, but when doing so, the tableview stops reacting to those two gestures (it still reacts to swipes).
I've made it work with the following code, but it's obviously not a nice solution and I'm sure there is a better way. This is put in the UIView (the superview):
-(UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
if([super hitTest:point withEvent:event] == self) {
for (id gesture in self.gestureRecognizers) {
[gesture setEnabled:YES];
}
return self;
}
for (id gesture in self.gestureRecognizers) {
[gesture setEnabled:NO];
}
return [self.subviews lastObject];
}
I had a very similar problem and found my solution in this SO question. In summary, set yourself as the delegate for your UIGestureRecognizer and then check the targeted view before allowing your recognizer to process the touch. The relevant delegate method is:
- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer
shouldReceiveTouch:(UITouch *)touch
The blocking of touch events to subviews is the default behaviour. You can change this behaviour:
UITapGestureRecognizer *r = [[UITapGestureRecognizer alloc] initWithTarget:self action:#selector(agentPickerTapped:)];
r.cancelsTouchesInView = NO;
[agentPicker addGestureRecognizer:r];
I was displaying a dropdown subview that had its own tableview. As a result, the touch.view would sometimes return classes like UITableViewCell. I had to step through the superclass(es) to ensure it was the subclass I thought it was:
-(BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldReceiveTouch:(UITouch *)touch
{
UIView *view = touch.view;
while (view.class != UIView.class) {
// Check if superclass is of type dropdown
if (view.class == dropDown.class) { // dropDown is an ivar; replace with your own
NSLog(#"Is of type dropdown; returning NO");
return NO;
} else {
view = view.superview;
}
}
return YES;
}
Building on #Pin Shih Wang answer. We ignore all taps other than those on the view containing the tap gesture recognizer. All taps are forwarded to the view hierarchy as normal as we've set tapGestureRecognizer.cancelsTouchesInView = false. Here is the code in Swift3/4:
func ensureBackgroundTapDismissesKeyboard() {
let tapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(handleTap))
tapGestureRecognizer.cancelsTouchesInView = false
self.view.addGestureRecognizer(tapGestureRecognizer)
}
#objc func handleTap(recognizer: UIGestureRecognizer) {
let location = recognizer.location(in: self.view)
let hitTestView = self.view.hitTest(location, with: UIEvent())
if hitTestView?.gestureRecognizers?.contains(recognizer) == .some(true) {
// I dismiss the keyboard on a tap on the scroll view
// REPLACE with own logic
self.view.endEditing(true)
}
}
One possibility is to subclass your gesture recognizer (if you haven't already) and override -touchesBegan:withEvent: such that it determines whether each touch began in an excluded subview and calls -ignoreTouch:forEvent: for that touch if it did.
Obviously, you'll also need to add a property to keep track of the excluded subview, or perhaps better, an array of excluded subviews.
It is possible to do without inherit any class.
you can check gestureRecognizers in gesture's callback selector
if view.gestureRecognizers not contains your gestureRecognizer,just ignore it
for example
- (void)viewDidLoad
{
UITapGestureRecognizer *singleTapGesture = [[UITapGestureRecognizer alloc] initWithTarget:self action:#selector(handleSingleTap:)];
singleTapGesture.numberOfTapsRequired = 1;
}
check view.gestureRecognizers here
- (void)handleSingleTap:(UIGestureRecognizer *)gestureRecognizer
{
UIEvent *event = [[UIEvent alloc] init];
CGPoint location = [gestureRecognizer locationInView:self.view];
//check actually view you hit via hitTest
UIView *view = [self.view hitTest:location withEvent:event];
if ([view.gestureRecognizers containsObject:gestureRecognizer]) {
//your UIView
//do something
}
else {
//your UITableView or some thing else...
//ignore
}
}
I created a UIGestureRecognizer subclass designed for blocking all gesture recognizers attached to a superviews of a specific view.
It's part of my WEPopover project. You can find it here.
implement a delegate for all the recognizers of the parentView and put the gestureRecognizer method in the delegate that is responsible for simultaneous triggering of recognizers:
func gestureRecognizer(UIGestureRecognizer, shouldBeRequiredToFailByGestureRecognizer:UIGestureRecognizer) -> Bool {
if (otherGestureRecognizer.view.isDescendantOfView(gestureRecognizer.view)) {
return true
} else {
return false
}
}
U can use the fail methods if u want to make the children be triggered but not the parent recognizers:
https://developer.apple.com/reference/uikit/uigesturerecognizerdelegate
I was also doing a popover and this is how I did it
func didTap(sender: UITapGestureRecognizer) {
let tapLocation = sender.locationInView(tableView)
if let _ = tableView.indexPathForRowAtPoint(tapLocation) {
sender.cancelsTouchesInView = false
}
else {
delegate?.menuDimissed()
}
}
You can turn it off and on.... in my code i did something like this as i needed to turn it off when the keyboard was not showing, you can apply it to your situation:
call this is viewdidload etc:
NSNotificationCenter *center = [NSNotificationCenter defaultCenter];
[center addObserver:self selector:#selector(notifyShowKeyboard:) name:UIKeyboardDidShowNotification object:nil];
[center addObserver:self selector:#selector(notifyHideKeyboard:) name:UIKeyboardWillHideNotification object:nil];
then create the two methods:
-(void) notifyShowKeyboard:(NSNotification *)inNotification
{
tap.enabled=true; // turn the gesture on
}
-(void) notifyHideKeyboard:(NSNotification *)inNotification
{
tap.enabled=false; //turn the gesture off so it wont consume the touch event
}
What this does is disables the tap. I had to turn tap into a instance variable and release it in dealloc though.