Multiple Gesture Responders for a Single View - ios

I have an image that I would like to set up to respond to several different gesture responders. So for example, if one part of the picture is touched I would like one selector to be called, and another selector for a different part of the picture.
I looked at the UIGestureRecognizer and UITapGestureRecognizer classes, but I couldn't find a way to specify the image zones to be associated with them. Is this at all possible in iOS? And if so what classes should I look into using?

The easiest solution is to lay invisible views over the image and put the gesture recognizers on them.
If that's not feasible you'll have to look at the locationInView in the gesture recognizer's tap handler and figure out what you want to do based on where the user tapped.

Use the locationInView: property to determine where your tap occurred and then conditionally invoke a method. You can do this by setting up some CGRects that correspond to your hit areas. Then use the CGRectContainsPoint() function to determine if the tap landed in one of the hit areas.
Your tap gesture recognizer action may look something like this:
- (void)tapGestureRecognized:(UIGestureRecognizer *)recognizer
{
// Specify some CGRects that will be hit areas
CGRect firstHitArea = CGRectMake(10.0f, 10.0f, 44.0f, 44.0f);
CGRect secondHitArea = CGRectMake(64.0f, 10.0f, 44.0f, 44.0f)
// Get the location of the touch in the view's coordinate space
CGPoint touchLocation = [recognizer locationInView:recognizer.view];
if (CGRectContainsPoint(firstHitArea, touchLocation))
{
[self firstMethod];
}
else if (CGRectContainsPoint(secondHitArea, touchLocation))
{
[self secondMethod];
}
}

Related

Forward UITouch event from superview to a UIControl subclass

I'm writing a radial menu, where when you long press (UILongPressGestureRecognizer) on the screen, it pops out a menu of buttons, and I can drag my finger (which is already touching the screen) over one of the buttons, which selects, then when I let go, it performs an action specific to that button.
I currently have the radial menu as a UIControl subclass, and I'm trying to override beginTrackingWithTouch: and continueTrackingWithTouch:, but the long press that shows the menu (adds it to the superview), does not get transferred to a touch recognized by the UIControl.
Any ideas how I can "forward" this touch event from the UIControl's superview to it?
Thanks!
Not a direct answer, but you should really watch the WWDC session about scrollviews of this year. And then watch it again. It contains a fantastic amount of information, and most certainly an answer to your question. It is session 235: advanced scrollviews and touch handling techniques.
I would do this...
The long press handler:
-(IBAction)onLongPress:(UILongPressGestureRecognizer*)recognizer
{
CGPoint point = [recognizer locationInView:self.view];
if (recognizer.state == UIGestureRecognizerStateBegan) {
//create the radial view and add it to the view
CGSize radialViewSize = CGSizeMake(80, 80);
radialView = [[RadialView alloc] initWithFrame:CGRectMake(point.x - radialViewSize.width/2, point.y - radialViewSize.height/2, radialViewSize.width, radialViewSize.height)];
[self.view addSubview:radialView];
radialView.backgroundColor = [UIColor redColor];
} else if (recognizer.state == UIGestureRecognizerStateEnded) {
[radialView onTouchUp:[radialView convertPoint:point fromView:self.view]];
[radialView removeFromSuperview];
radialView = nil;
}
}
In your radial view: (I suppose that the radial view keeps the buttons in an array)
-(void)onTouchUp:(CGPoint)point
{
for (UIButton *button in buttons) {
if ([button pointInside:[self convertPoint:point toView:button] withEvent:nil]) {
//This button got clicked
//send button clicked event
[button sendActionsForControlEvents:UIControlEventTouchUpInside];
}
}
}
I know it's not perfect, since the touch events don't get forwarded to the radial view (as you asked), but it let's you click the buttons. Hope it helps!
I'm not sure if this is the same behavior you're looking for, but I recently had to overcome the exact same issue when developing my Concentric Radial Menu. The thing I discovered very quickly was that views added to the view hierarchy during the touch event do not get re-hit-tested and therefore seem unresponsive until the next event comes around.
The solution I used, which I can't say I love, was to implement a custom UIWindow subclass that intercepts - (void)sendEvent:(UIEvent *)event and forwards those events to the "active" radial menu.
That is, the menu registers with the window upon activation, then unregisters when being unloaded. If done atomically, this is actually a pretty safe technique, I just wish it were cleaner than it is.
Best of luck!

How to get coordinates of the tap in 2 views simultaneously

I am working on an iOS map app and it includes interactive map. The interactive map is a subclass of UIImageView and placed on a scrollView. My view hierarchy is shown below:
When user taps some part of the map, ViewController performs animated segue (like zoom-in to that area of the map). I can start segue from any point of the screen, but to do this properly, I need exact coordinates of user's tap relative to the screen itself. As ImageView is put at the top of ScrollView, it uses different coordinate system, larger than screen size. No matter, which area of map has ben tapped, what matters is the tapped CGPoint on the screen (physical).
ImageView uses its own code to get coordinates of a tap:
-(void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event
{
[super touchesEnded:touches withEvent:event];
// cancel previous touch ended event
[NSObject cancelPreviousPerformRequestsWithTarget:self];
CGPoint touchPoint = \
[[touches anyObject] locationInView:self];
NSValue* touchValue =\
[NSValue
valueWithCGPoint:touchPoint];
// perform new one
[self
performSelector:#selector(_performHitTestOnArea:)
withObject:touchValue
afterDelay:0.1];
}
And the case if I place gesture recognizer, it works, but ImageView can't get any touches and, therefore, call segue.
The code for gesture recognizer, I attempted to use:
UITapGestureRecognizer *rec = [[UITapGestureRecognizer alloc] initWithTarget:self action:#selector(tapRecognized:)];
[someView addGestureRecognizer:rec];
[rec release];
// elsewhere
- (void)tapRecognized:(UITapGestureRecognizer *)recognizer
{
if(recognizer.state == UIGestureRecognizerStateRecognized)
{
CGPoint point = [recognizer locationInView:recognizer.view];
// again, point.x and point.y have the coordinates
}
}
So, is there any way to get two coordinates in different reference systems?, or to make these recognizers work simultaneously without interfering each other?
Solved
I use this code to convert touched point from one view's reference system to
CGPoint pointInViewCoords = [self.parentView convertPoint:self.imageView.touchPoint fromView:self.imageView];
Where self.parentView is "View" on hierarchy image - with the size of the screen.

How to know if subViews intersect While moving with UIGesturerescognizer or some otherway?

In my application I have many UIButtons dynamically added to the View and I use below method
to Drag them around the View.
//forDragAction
[btnTarget addTarget:self action:#selector(wasDragged:withEvent:)
forControlEvents:UIControlEventTouchDragInside];
- (void)wasDragged:(UIButton *)button withEvent:(UIEvent *)event
{
// get the touch
UITouch *touch = [[event touchesForView:button] anyObject];
// get delta
CGPoint previousLocation = [touch previousLocationInView:button];
// frameof buttonChanged here
}
I want to stop dragging action if the dragged one intersect with anyother, I know that I can use for loop like below to check if any UIButton is Interacting
for(UIButton *btn in [[button superview] subViews])
{
//check if the btn frame interact with any others if so comeout of loop
}
I want to know if there is someother way, As mentioned way will get slower if the subViews count increse to such great amount
Edit:- the UIButtons are dynamically added to the UIView (But total amount of subViews won't exceed 120)
Try brute force first. You might be surprised at how well it does. (But remember that a loop through [[button superview] subviews] will contain the button itself, so it will always stop because the button intersects itself. Be sure to exclude the button).
Optimize after you have something working that is demonstrably slow with real data.
If that's really the case, there's a whole lot of algorithmic work done on this problem, which can be summarized as preprocessing the data into structures that allow cheaper initial tests to reject distant objects. This is a good SO answer on the topic, referring to this article.
I don't think that count of subviews, that may fill the screen (without intersections), is too large. So use function:
bool CGRectIntersectsRect (
CGRect rect1,
CGRect rect2
);
for detect whether frame of dragged button intersects with another subview.

Panning a UIImage with a Gesture Recognizer on a placeholder UIView

I need to crop an image to match a specific dimension. I have three layers in my view starting from the bottom:
I have the raw image in a UIImage. This image comes from the camera. (called cameraImage)
I have a UIView holding this image. When user clicks "crop", the UIView's bounds are used to crop the raw image inside it.
Above all of this I have a guide image which shows the user the dimensions they need to pan, rotate, and pinch their image to fit into.
I want to add the pan gesture to the top guide image and have it control the raw image at the bottom. So the guide image never moves but it is listening for the pan gesture.
I can't figure out how to reset the recognizer without it making my raw image jump back to zero. Maybe someone could help?
- (IBAction)handlePan:(UIPanGestureRecognizer *)recognizer {
CGPoint translation = [recognizer translationInView:recognizer.view];
recognizer.view.center = CGPointMake(recognizer.view.center.x+translation.x, recognizer.view.center.y+ translation.y);
[recognizer setTranslation:CGPointMake(0, 0) inView:recognizer.view];
}
The above code works great in my gesture is attached to the bottom image. The problem is when the user goes outside the view's bounds, the image stops panning and is basically stuck. You can't touch it anymore so it sits there. So I thought if i attached the gesture to the top it would solve this problem.
- (IBAction)handlePan:(UIPanGestureRecognizer *)recognizer {
CGPoint translation = [recognizer translationInView:recognizer.view];
cameraImage.center = CGPointMake(recognizer.view.center.x+translation.x, recognizer.view.center.y+ translation.y);
}
This almost works. I set the cameraImage's center and removed the third line which resets the recognizer. If I don't remove it, the cameraImage jumps back into the same position every time I try and pan. It almost works because when you click the image again it doesn't start from the pixel you touched. It moves the image back to the original position and then lets you pan.
First option:
when the recognizer enter the UIGestureRecognizerStateEndedstate
if(recofnizer.state == UIGestureRecognizerStateEnded )
{
...
}
You store the translation at that point in time in an instance varibale (#property) of your class.
And then you always add the saved translation to the new translation.
In code this would look like this:
- (IBAction)handlePan:(UIPanGestureRecognizer *)recognizer {
CGPoint translation = [recognizer translationInView:recognizer.view];
CGPoint updatedTranslation = CGPointMake(translation.x+self.savedTranslation.x,translation.y+self.savedTranslation.y);
cameraImage.center = CGPointMake(recognizer.view.center.x+updatedTranslation.x, recognizer.view.center.y+ updatedTranslation.y);
if(recofnizer.state == UIGestureRecognizerStateEnded )
{
self.savedTranslation = updatedTranslation;
}
}
Dont forget to add #property (nonatomic, assign) CGPoint savedTranslation; to your interface.
Also make sure the savedTranslation variable is initialized in the init method of your class to self.savedTranslation = CGPointMake(0,0);
Second option:
You should think about doing all that what you want in an scrollview with the imageview as the viewForZooming of the scrollview. This allows very smooth interaction, like users are used to!
Above this scrollview you can then put your mask/guide, but make sure to disable the userInteraction of the mask/guide view to make your user touch the scrollview below!

iOS: Transfer ownership of UIView while touches happening?

The moment I receive touchesBegan, I want to removeFromSuperview the view that was touched and addSuperview to a new parent view, and then continue to receive touches. However I am finding that sometimes this does not work. Specifically, touchesMoved and touchesEnded are never called.
Is there a trick for making this work correctly? This is for implementing a drag and drop behavior, where the view is initially inside a scroll view.
Thanks.
Instead of:
[transferView removeFromSuperView];
[newParentView addSubview:transferView];
Use only:
[newParentView addSubview:transferView];
The documentation states: "Views can have only one superview. If view already has a superview and that view is not the receiver, this method removes the previous superview before making the receiver its new superview."
Therefore there is no need to use removeFromSuperView because it is handled by addSubview. I have noticed that removeFromSuperView ends any current touches without calling touchesEnded. If you use only addSubview, touches are not interrupted.
You need to process your touches in the superview instead of in the view that you want switched out. This will allow you to switch out the view without loosing your touch events. When you do this though, you'll have to test yourself whether the touch is occurring in the specific subview you want switched out. This can be done many ways, but here are some methods to get you started:
Converting Rects/Point to another view:
[view convertRect:rect toView:subview];
[view convertPoint:point toView:subview];
Here are some methods to test if the point is located in the view:
[subView hitTest:point withEvent:nil];
CGRectContainsPoint(subview.frame, point); //No point conversion needed
[subView pointInside:point withEvent:nil];
In general, it's better to use UIGestureRecognizers. For example, if you were using a UIPanGestureRecognizer, you would create a method that the gesture recognizer can call and in that method you do your work. For example:
- (void) viewPanned:(UIPanGestureRecognizer *)pan{
if (pan.state == UIGestureRecognizerStateBegan){
CGRect rect = subView.frame;
newView = [[UIView alloc] initWithFrame:rect];
[subView removeFromSuperview];
[self addSubview:newView];
} else if (pan.state == UIGestureRecognizerStateChanged){
CGPoint point = [pan locationInView:self];
newView.center = point;
} else {
//Do cleanup or final view placement
}
}
Then you init the recognizer, assign it to the target (usually self) and add it:
[self addGestureRecognizer:[[UIPanGestureRecognizer alloc] initWithTarget:self action:#selector(viewPanned:)]];
Now self (which would be the superview managing it's subviews) will respond to pan motions.

Resources