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.
Related
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.
I have a UIScrollView filled with draggable (UIView) cards as subviews and I want the cards to re-organize themselves (make space for new when the user drags one of them into the UIScrollView.
The problem is: how do I know which of the UIViews is under the one I'm dragging, so I can get its index and move it away from the card being dragged?
I tried using hitTest:withEvent: but I think I'm far from doing it right, since it's returning nil.
UIView *viewUnderCard = [card hitTest:card.center withEvent:nil];
Just started developing for iOS. Any help please?
You're on the right track, hitTest:withEvent: can be used. But #Mysiaq is correct that pointInside:withEvent: is probably even better.
You need to make sure that the coordinates are relative to the correct view. If you're using card.center, the coordinate system is that of the card's parent view.
The code could look something like this:
UIView *container = viewThatHasAllTheCards;
UIView *targetCard = nil;
CGPoint cardInWindow = [draggedCard.superview convertPoint:draggedCard.center toView:nil];
CGPoint cardInContainer = [container convertPoint:cardInWindow fromView:nil];
for (UIView *subview in container.subviews) {
if (subview == draggedCard) {
// Skip the dragged card.
continue;
}
if ([subview pointInside:cardInContainer withEvent:nil]) {
targetCard = subview;
// If you want the lower-most card, break here.
// If you want the top-most card, do not break here.
}
}
Get point of your touch and then call function
- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event
for all UIScrollView subviews and function will return you YES if provided CGPoint is inside of its frame.
You can compare bounding rects of two views:
CGRect boundsView1 = [view1 convertRect:view1.bounds toView:nil];
CGRect boundsView2 = [view2 convertRect:view2.bounds toView:nil];
Boolean viewsOverlap = CGRectIntersectsRect(boundsView1, boundsView2);
From here, you should be able to figure out how to iterate efficiently through your list of views to determine if any overlap.
I've been experimenting with UIGestureRecognizers and the new SKScene/SKNode's in SpriteKit. I've had one problem, and I got close to fixing it but I am confused on one thing. Essentially, I have a pan gesture recognizer that allows the user to drag a sprite on the screen.
The single problem I have is that it takes ONE tap to actually initialize the pan gesture, and then only on the SECOND tap on it works correctly. I'm thinking that this is because my pan gesture is initialized in touchesBegan. However, I don't know where else to put it since initializing it in the SKScene's initWithSize method stopped the gesture recognizer from actually working.
-(void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
if (!self.pan) {
self.pan = [[UIPanGestureRecognizer alloc]initWithTarget:self action:#selector(dragPlayer:)];
self.pan.minimumNumberOfTouches = 1;
self.pan.delegate = self;
[self.view addGestureRecognizer:self.pan];
}
}
-(void)dragPlayer: (UIPanGestureRecognizer *)gesture {
CGPoint trans = [gesture translationInView:self.view];
SKAction *moveAction = [SKAction moveByX:trans.x y:-trans.y duration:0];
[self.player runAction:move];
[gesture setTranslation:CGPointMake(0, 0) inView:self.view];
}
That's because you're adding the gesture in touches began, so the gesture doesn't exist until the screen has been tapped at least once. Additionally, I would verify that you're actually using initWithSize: as your initializer, because you shouldn't have any problems adding the gesture there.
Another option is to move the code to add the gesture into -[SKScene didMovetoView:] which gets called immediately after the scene has been presented. More info in the docs.
- (void)didMoveToView:(SKView *)view
{
[super didMoveToView:view];
// add gesture here!
}
This is my first post! Hoping to not trip over my own toes...
Hi guys, so I was having an issue with a UISwipeGestureRecognizer not working. I was initializing it in my initWithSize method so based on this post I moved it to my didMoveToView method. Now it works (thanks 0x7fffffff). All I did was cut the following two lines from one method and paste them in the other.
_warpGesture = [[UISwipeGestureRecognizer alloc] initWithTarget:self action:#selector(warpToNextLevel:)];
[self.view addGestureRecognizer:_warpGesture];
In my "investigation" I came across userInteractionEnabled and tried to set it to YES in my initWithSize method...
self.view.userInteractionEnabled = YES;
NSLog(#"User interaction enabled %s", self.view.userInteractionEnabled ? "Yes" : "No");
This would log NO even though i'd just set it to YES. Further investigation found that if I don't try to manually set userInteractionEnabled then it's NO during initWithSize (I can't seem to change this if I want to) and automatically gets set to YES when i'm in didMoveToView.
This all strikes me as relevant but I would love for someone in the know to explain just what's going on here. Thanks!
My interface sometimes has buttons around its periphery. Areas without buttons accept gestures.
GestureRecognizers are added to the container view, in viewDidLoad. Here’s how the tapGR is set up:
UITapGestureRecognizer *tapGR = [[UITapGestureRecognizer alloc] initWithTarget:self action:#selector(playerReceived_Tap:)];
[tapGR setDelegate:self];
[self.view addGestureRecognizer:tapGR];
In order to prevent the gesture recognizers from intercepting button taps, I implemented shouldReceiveTouch to return YES only if the view touched is not a button:
- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gr
shouldReceiveTouch:(UITouch *)touch {
// Get the topmost view that contains the point where the gesture started.
// (Buttons are topmost, so if they were touched, they will be returned as viewTouched.)
CGPoint pointPressed = [touch locationInView:self.view];
UIView *viewTouched = [self.view hitTest:pointPressed withEvent:nil];
// If that topmost view is a button, the GR should not take this touch.
if ([viewTouched isKindOfClass:[UIButton class]])
return NO;
return YES;
}
This works fine most of the time, but there are a few buttons that are unresponsive. When these buttons are tapped, hitTest returns the container view, not the button, so shouldReceiveTouch returns YES and the gestureRecognizer commandeers the event.
To debug, I ran some tests...
The following tests confirmed that the button was a sub-subview of the container view, that it was enabled, and that both button and the subview were userInteractionEnabled:
-(BOOL)gestureRecognizer:(UIGestureRecognizer *)gr
shouldReceiveTouch:(UITouch *)touch {
// Test that hierarchy is as expected: containerView > vTop_land > btnSkipFwd_land.
for (UIView *subview in self.view.subviews) {
if ([subview isEqual:self.playComposer.vTop_land])
printf("\nViewTopLand is a subview."); // this prints
}
for (UIView *subview in self.playComposer.vTop_land.subviews) {
if ([subview isEqual:self.playComposer.btnSkipFwd_land])
printf("\nBtnSkipFwd is a subview."); // this prints
}
// Test that problem button is enabled.
printf(“\nbtnSkipFwd enabled? %d", self.playComposer.btnSkipFwd_land.enabled); // prints 1
// Test that all views in hierarchy are interaction-enabled.
printf("\nvTopLand interactionenabled? %d", self.playComposer.vTop_land.userInteractionEnabled); // prints 1
printf(“\nbtnSkipFwd interactionenabled? %d", self.playComposer.btnSkipFwd_land.userInteractionEnabled); // prints 1
// etc
}
The following test confirms that the point pressed is actually within the button’s frame.
- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gr
shouldReceiveTouch:(UITouch *)touch {
CGPoint pointPressed = [touch locationInView:self.view];
CGRect rectSkpFwd = self.playComposer.btnSkipFwd_land.frame;
// Get the pointPressed relative to the button's frame.
CGPoint pointRelSkpFwd = CGPointMake(pointPressed.x - rectSkpFwd.origin.x, pointPressed.y - rectSkpFwd.origin.y);
printf("\nIs relative point inside skipfwd? %d.", [self.playComposer.btnSkipFwd_land pointInside:pointRelSkpFwd withEvent:nil]); // prints 1
// etc
}
So why is hitTest returning the container view rather than this button?
SOLUTION: The one thing I wasn't testing was that the intermediate view, vTop_land, was framed properly. It looked OK because it had an image that extended across the screen -- past the bounds of its frame (I didn't know this was possible). The frame was set to portrait width, rather than landscape width, so buttons on the far right were out of zone.
Hit test is not reliable in most cases, and it is generally not advisable to use it along with gestureRecognizers.
Why dont you setExclusiveTouch:YES for each button, and this should make sure that the buttons are always chosen.
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];
}
}