Sibling subview does not receive touches in iPhone - iphone

I have a view (parent) with two subviews, one on top (topChild) of the other (bottomChild).
If I tap on the screen only topChild and parent receive the touch event.
What should I change to propagate the touch event to bottomChild as well?
The code:
- (void)viewDidLoad
{
[super viewDidLoad];
MYView* parent = [[MYView alloc] initWithFrame:CGRectMake(0, 0, 100, 100)];
parent.tag = 3;
parent.backgroundColor = [UIColor redColor];
MYView* bottomChild = [[MYView alloc] initWithFrame:CGRectMake(0, 0, 90, 90)];
bottomChild.tag = 2;
bottomChild.backgroundColor = [UIColor blueColor];
[parent addSubview:bottomChild];
MYView* topChild = [[MYView alloc] initWithFrame:CGRectMake(0, 0, 80, 80)];
topChild.tag = 1;
topChild.backgroundColor = [UIColor greenColor];
[parent addSubview:topChild];
[self.view addSubview:parent];
}
Where MYView is a subclass of UIView that only logs touchesBegan.
- (void) touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
NSLog(#"%d", self.tag);
[super touchesBegan:touches withEvent:event];
}
The result:
Touching the green area produces the following log:
TouchTest[25062:f803] 1
TouchTest[25062:f803] 3
My first idea was to make parent propagate all the touchesSomething calls to its children but (A) I suspect there might be an easier solution and (B) I don't know which child sent the event to parent, and sending touchesSomething messages twice to the same view might cause shenanigans.
After asking the question I've found this post that suggests to override hitTest to change the view that receives the touches. I will try this approach and update the question if it works.

It's an interesting problem you have that is probably something best addressed through a rethink of the way you have things structured. But to make it work the way you suggest you need to catch the touch event in the current top view, pass it to the parent and then propagate it down through all subviews of the parent view. To make this work you would need the touchesBegan: (or whatever other method you use to intercept the touch) to do nothing in all the views, doing the action only in the method called by the parent view.
Which is really another way of saying don't handle touches in the views, catch them but notify the parent view view and then call subview methods as required to cause the effect you want.
- (void) touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
// Do nothing, parent view calls my parentNotifiedTouchesBegan method
[self.superview touchesBegan:touches withEvent:event];
}
- (void) parentNotifiedTouchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
// Act on the touch here just as my sibling views are doing
}
Note I changed super to self.superview in that code. You may or may not also want to call super's method depending on what you are doing, and the place to call that may be in parentNotifiedTouchesBegan.
You can know which subView sent the event of course, just use a custom method to notify the superview instead of calling its touchesBegan:. Use a self argument.

If you don't need the touches on the children then set
bottomChild.userInteractionEnabled = NO;
topChild.userInteractionEnabled = NO;

Related

Restrict user to one finger touch on UIView to avoid frame issues

I know there are lots of questions on multi finger touches and restrictions here and there,but none of them seem to work.All they suggest is to try setting exclusive touch and disable multi touch i.e.:
[self.view.subviews makeObjectsPerformSelector:#selector(setExclusiveTouch:) withObject:[NSNumber numberWithBool:YES]];
[self.view setMultipleTouchEnabled:NO];
Note: Initially when I used self.view.exclusiveTouch = NO; ,it didn't work for me.Mr.Hlung's comment in the 1st link provided some useful info of enabling the subviews exclusive touch through selector.
But they seem to work incomplete.I have a game view where there are multiple images on left and their corresponding images on the right.User has to drag the left image to the respective right image and match it.I am using UIPanGestureRecognizer to do so.Here comes a scenario which was the outcome of some rash testing by a tester in my office :)
When the user scrambles the images,i.e. plays awkwardly with all/few of his fingers,what happens is the frames are getting disturbed or few images overlap with one another.This is all because some where multiple touch is still available for user.Hence I want to eliminate that feature.I am also aware of the fact that there is a method called -(void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event ,through which we can detect the number of user touches.What if I delete all user touches if the count is more than 1,i.e. if more than 1 finger touch is detected!!!
But I found no method to remove touches there,I also tried to assign last touch object to my image view through which I thought would ignore the rest of touches detected,i.e.:
-(void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
if ([event allTouches].count>1)
{
UITouch *touch = [[touches allObjects] lastObject];
self.dragObjectImageView = (UIImageView *)[touch view];
}
}
Even this doesn't seem to work.
EDIT-Setting Max and Min number of touches:
I have also set the pan gesture's max and min no of touches to 1 as well,i.e.:
- (void)viewDidLoad
{
[super viewDidLoad];
[self.view.subviews makeObjectsPerformSelector:#selector(setExclusiveTouch:) withObject:[NSNumber numberWithBool:YES]];
for(UIImageView *iView in self.movableArray){
if ([iView isMemberOfClass:[UIImageView class]]){
UIPanGestureRecognizer * recognizer = [[UIPanGestureRecognizer alloc] initWithTarget:self action:#selector(handlePan:)];
recognizer.minimumNumberOfTouches = 1;
recognizer.maximumNumberOfTouches = 1;
iView.multipleTouchEnabled = NO;
[iView addGestureRecognizer:recognizer];
[iView setUserInteractionEnabled:YES];
recognizer.delegate = self;
}
}
}
Tried another tactic of disabling user interaction for image if multiple touches are detected,but in vain!
-(void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
if ([event allTouches].count > 1)
{
for (UIView* view in self.view.subviews)
{
if ([view isKindOfClass:[UIImageView class]])
{
[view setUserInteractionEnabled:NO];
}
}
}
else
{
for (UIView* view in self.view.subviews)
{
if ([view isKindOfClass:[UIImageView class]])
{
[view setUserInteractionEnabled:YES];
}
}
}
}
Some people might say there can't be a case where user will use multiple fingers to play.But I was asked to fix the issue and a developer has to obey and respect the testers perspective as well.
Can some one please help me get rid of the problem.
Thanks every one in advance :)
Instead of setting exclusive touch to our own view,turning on the same for the subview or the view we assign to self.view worked,in my case it is...
[gameView.subviews makeObjectsPerformSelector:#selector(setExclusiveTouch:) withObject:[NSNumber numberWithBool:YES]];
Hope it helps some one,thanks :)
As per apple documentation for the property multipleTouchEnabled in UIView Class Reference
multipleTouchEnabled
Other views in the same window can still receive touch events when
this property is NO. If you want this view to handle multi-touch
events exclusively, set the values of both this property and the
exclusiveTouch property to YES.
I see in your code, you've set setExclusiveTouch to all subviews but setMultipleTouchEnabled only for the container view and not all image views. Add the following line before/after setting exclusive touch and single touch may work correctly.
[self.view.subviews makeObjectsPerformSelector:#selector(setMultipleTouchEnabled:) withObject:[NSNumber numberWithBool:NO]];

Touch and pull down a view

I have a uiview at the top of the interface (below the status bar) that only the bottom part of it is shown.
Actually, I want to make the red uiview to slide down to be entirely shown by drag such as the notificationcenter in the native iOS and not just by taping a button.
What should I use to "touch and pull down" the uiview so it could be shown entirely ?
No needs to find a workaround of drag-n-drop. An UIScrollView can do it without any performance loss brought by listening on touches.
#interface PulldownView : UIScrollView
#end
#implementation PulldownView
- (id)initWithFrame:(CGRect)frame
{
self = [super initWithFrame:frame];
if (!self) {
return self;
}
self.pagingEnabled = YES;
self.bounces = NO;
self.showsVerticalScrollIndicator = NO;
[self setBackgroundColor:[UIColor clearColor]];
double pixelsOutside = 20;// How many pixels left outside.
self.contentSize = CGSizeMake(320, frame.size.height * 2 - pixelsOutside);
// redArea is the draggable area in red.
UIView *redArea = [[UIView alloc] initWithFrame:frame];
redArea.backgroundColor = [UIColor redColor];
[self addSubview:redArea];
return self;
}
// What this method does is to make sure that the user can only drag the view from inside the area in red.
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event
{
if (point.y > height)
{
// Leaving useless touches to the views under it.
return nil;
}
return [super hitTest:point withEvent:event];
}
#end
How to use:
1. Initialize an instance of PulldownView.
2. Add any content you want to display to the instance using [addSubview:].
3. Hide the area in red.
[pulldownView setContentOffset:CGPointMake(0, heightOfTheView - pixelsOutside)];
This is a simple example. You can add any features to it like adding a titled button bar on the bottom of the draggable area to implement click-n-drop, or adding some method to the interface to reposition it by the caller.
Make a subclass of UIView.
Override touchesBegan:withEvent and touchesMoved:withEvent.
In the touchesBegan perhaps make a visual change so the user knows they are touching the view.
In the touchesMoved use
[[touches anyObject] locationInView:self]
and
[[touches anyObject] previousLocationInView:self]
to calculate the difference between the current touch position and the last touch position (detect drag down or drag back up).
Then if you're custom drawing, call [self setNeedsDisplay] to tell your view to redraw in it's drawRect:(CGRect)rect method.
Note: this assumes multiple touch is not used by this view.
Refer to my answer in iPhone App: implementation of Drag and drop images in UIView
You just need to use TouchesBegin and TouchesEnded methods. In that example, I have shown how to use CGPoint, Instead of that you have to try to use setFrame or drawRect for your view.
As soon as TouchesMoved method is called you have to use setFrame or drawRect (not sure but which ever works, mostly setFrame) also take the height from CGPoint.

Change the text in a subview dynamically

I have created a 3 Subviews to MyView by creating new class inherits from UIView.
then i tried to change text in a particular subview say second sub view.
But its not updating.So how to do this...
EDIT
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
CALayer *hitLater = [self layerForTouch:[touches anyObject]];
if ([hitLater isKindOfClass:[Tile class]])
{
Tile *tile = (Tile *)hitLater;
heldTile = tile;
[tile draw2];
[tile setNeedsDisplay];
}
}
Thanks in advance..
If you want a better answer show some code and how you add the text to your UIView subclasses etc.
But maybe you have to call setNeedsDisplay on your subviews so they get a redraw.

How to disable touch input to all views except the top-most view?

I have a view with multiple subviews. When a user taps a subview, the subview expands in size to cover most of the screen, but some of the other subviews are still visible underneath.
I want my app to ignore touches on the other subviews when one of the subviews is "expanded" like this. Is there a simple way to achieve this? I can write code to handle this, but I was hoping there's a simpler built-in way.
Hope this help...
[[yourSuperView subviews]
makeObjectsPerformSelector:#selector(setUserInteractionEnabled:)
withObject:[NSNumber numberWithBool:FALSE]];
which will disable userInteraction of a view's immediate subviews..Then give userInteraction to the only view you wanted
yourTouchableView.setUserInteraction = TRUE;
EDIT:
It seems in iOS disabling userInteraction on a parent view doesn't disable userInteraction on its childs.. So the code above (I mean the one with makeObjectsPerformSelector:)will only work to disable userInteraction of a parent's immediate subviews..
See user madewulf's answer which recursively get all subviews and disable user interaction of all of them. Or if you need to disable userInteraction of this view in many places in the project, You can categorize UIView to add that feature.. Something like this will do..
#interface UIView (UserInteractionFeatures)
-(void)setRecursiveUserInteraction:(BOOL)value;
#end
#implementation UIView(UserInteractionFeatures)
-(void)setRecursiveUserInteraction:(BOOL)value{
self.userInteractionEnabled = value;
for (UIView *view in [self subviews]) {
[view setRecursiveUserInteraction:value];
}
}
#end
Now you can call
[yourSuperView setRecursiveUserInteraction:NO];
Also user #lxt's suggestion of adding an invisible view on top of all view's is one other way of doing it..
There are a couple of ways of doing this. You could iterate through all your other subviews and set userInteractionEnabled = NO, but this is less than ideal if you have lots of other views (you would, after all, have to subsequently renable them all).
The way I do this is to create an invisible UIView that's the size of the entire screen that 'blocks' all the touches from going to the other views. Sometimes this is literally invisible, other times I may set it to black with an alpha value of 0.3 or so.
When you expand your main subview to fill the screen you can add this 'blocking' UIView behind it (using insertSubview: belowSubview:). When you minimize your expanded subview you can remove the invisible UIView from your hierarchy.
So not quite built-in, but I think the simplest approach. Not sure if that was what you were thinking of already, hopefully it was of some help.
Beware of the code given as solution here by Krishnabhadra:
[[yourSuperView subviews]makeObjectsPerformSelector:#selector(setUserInteractionEnabled:) withObject:[NSNumber numberWithBool:FALSE]];
This will not work in all cases because [yourSuperView subviews] only gives the direct subviews of the superview. To make it work, you will have to iterate recursively on all subviews:
-(void) disableRecursivelyAllSubviews:(UIView *) theView
{
theView.userInteractionEnabled = NO;
for(UIView* subview in [theView subviews])
{
[self disableRecursivelyAllSubviews:subview];
}
}
-(void) disableAllSubviewsOf:(UIView *) theView
{
for(UIView* subview in [theView subviews])
{
[self disableRecursivelyAllSubviews:subview];
}
}
Now a call to disableAllSubviewsOf will do what you wanted to do.
If you have a deep stack of views, the solution by lxt is probably better.
I would do this by putting a custom transparent button with the same frame as the superView. And then on top of that button I would put view that should accept user touches.
Button will swallow all touches and views behind it wouldn't receive any touch events, but view on top of the button will receive touches normally.
Something like this:
- (void)disableTouchesOnView:(UIView *)view {
UIButton *ghostButton = [[UIButton alloc] initWithFrame:CGRectMake(0, 0, view.frame.size.width, view.frame.size.height)];
[ghostButton setBackgroundColor:[UIColor clearColor]];
ghostButton.tag = 42; // Any random number. Use #define to avoid putting numbers in code.
[view addSubview:ghostButton];
}
And a method for enabling the parentView.
- (void)enableTouchesOnView:(UIView *)view {
[[view viewWithTag:42] removeFromSuperview];
}
So, to disable all views in the parentViev behind yourView, I would do this:
YourView *yourView = [[YourView alloc] initWithCustomInitializer];
// It is important to disable touches on the parent view before adding the top most view.
[self disableTouchesOnView:parentView];
[parentView addSubview:yourView];
Just parentView.UserInteractionEnabled = NO will do the work.
Parent view will disable user interaction on all the view's subviews. But enable it does not enable all subviews(by default UIImageView is not interactable). So an easy way is find the parent view and use the code above, and there is no need to iterate all subviews to perform a selector.
Add a TapGestureRecognizer to your "background view" (the translucent one which "grays out" your normal interface) and set it to "Cancels Touches In View", without adding an action.
let captureTaps = UITapGestureRecognizer()
captureTaps.cancelsTouchesInView = true
dimmedOverlay?.addGestureRecognizer(captureTaps)
I will give my 2 cents to this problem.
Iteratively run userInteractionEnabled = false it's one way.
Another way will be add a UIView like following.
EZEventEater.h
#import <UIKit/UIKit.h>
#interface EZEventEater : UIView
#end
EZEventEater.m
#import "EZEventEater.h"
#implementation EZEventEater
- (id)initWithFrame:(CGRect)frame
{
self = [super initWithFrame:frame];
if (self) {
// Initialization code
self.backgroundColor = [UIColor clearColor];
self.userInteractionEnabled = false;
}
return self;
}
- (void) touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
//EZDEBUG(#"eater touched");
}
- (void) touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event
{
}
- (void) touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event
{
}
In your code you add the EZEventEater view to cover all the views that your may block the touch event.
Whenever you want to block the touch event to those views, simply call
eater.userInteractionEnabled = YES;
Hope this helpful.
In Swift 5, I achieved this behaviour by placing a view right on top(the highlighted one) and setting:
myView.isUserInteractionEnabled = true
This does not let the touches go through it, thus ignoring the taps.
For my app, I think it will be sufficient to disable navigation to other tabs of the app (for a limited duration, while I'm doing some processing):
self.tabBarController.view.userInteractionEnabled = NO;
Also, I disabled the current view controller--
self.view.userInteractionEnabled = NO;
(And, by the way, the recursive solutions proposed here had odd effects in my app. The disable seems to work fine, but the re-enable has odd effects-- some of the UI was not renabled).
Simple solution. Add a dummy gesture that does nothing. Make it reusable by adding it to an extension like this:
extension UIView {
func addNullGesture() {
let gesture = UITapGestureRecognizer(target: self,
action: #selector(nullGesture))
addGestureRecognizer(gesture)
}
#objc private func nullGesture() {}
}
setUserInteractionEnabled = NO on the view you want to disable
I had the same problem, but the above solutions did not help.
I then noticed that calling
super.touchesBegan(...) was the problem.
After removing this the event was only handled by the top-most view.
I hope this is of help to anybody.

Passing touches through a UIView to a UIScrollView not working

I have a view derived from a UIScrollView controller that I use to page through images in a library. That works fine.
I overlayed a UIView on the right side to handle touches for scrolling quickly through the list of images. My problem is that if I pass the touches through to either 'super' or 'nextResponder' they never make it to the UIScrollView object below.
My question is how can I force the UIScrollView below to handle the touches that I don't need to handle? I'm setting a timer for 0.3 seconds that during that time all touches are passed to the UIScrollView to handle. So if the user started a swipe gesture to turn the page, it will happen.
Here's the code for the touchesBegan method:
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
[super touchesBegan:touches withEvent:event];
// Start a timer to trigger the display of the slider and the processing of events.
touchesTimer = [NSTimer scheduledTimerWithTimeInterval:0.3 target:self selector:#selector(showSlider:) userInfo:nil repeats:NO];
// Where are we at right now?
UITouch *touch = [touches anyObject];
CGPoint currentPoint = [touch locationInView:self.view];
lastPage = [self page:currentPoint.y];
// Pass the event though until we need it.
// [self.nextResponder touchesBegan:touches withEvent:event];
if ([touch.view isKindOfClass:[UIScrollView class]])
{
if (self.nextResponder != nil &&
[self.nextResponder respondsToSelector:#selector(touchesEnded:withEvent:)])
{
[self.nextResponder touchesEnded:touches withEvent:event];
}
}
}
I would try using a regular UIViewController rather than a UIScrollViewController and then add the scroll view object where you need it and put the scroll faster view you want on the right side. This way they are separate and the touches won't get confused.
Here's an easier solutions that worked well for me:
In the UIView on top of the UIScrollview make the width and height both 0 (so that the view is no longer technically on top of the UIScrollView) and set clipsToBounds = NO (so that the contents of the view still show up on top of the UIScrollView).
self.OverlayView.clipsToBounds = NO;
CGRect frame = self.OverlayView.frame;
self.OverlayView.frame = CGRectMake(frame.origin.x, frame.origin.y, 0, 0);
Note that if the UIView contains interactive controls then they will no longer work. You'll need to move it into it's own view above the UIScrollView.