Sprite-Kit Pinch to Zoom Problems UIPinchGestureRecognizer - ios

I've been working on this code for quite a while now but it just feels like one step forward and two steps back. I'm hoping someone can help me.
I'm working with Sprite Kit so I have a Scene file that manages the rendering, UI and touch controls. I have an SKNode thats functioning as the camera like so:
_world = [[SKNode alloc] init];
[_world setName:#"world"];
[self addChild:_world];
I am using UIGestureRecognizer, so I add the ones I need like so:
_panRecognizer = [[UIPanGestureRecognizer alloc]initWithTarget:self action:#selector(handlePanFrom:)];
[[self view] addGestureRecognizer:_panRecognizer];
_pinchRecognizer = [[UIPinchGestureRecognizer alloc]initWithTarget:self action:#selector(handlePinch:)];
[[self view] addGestureRecognizer:_pinchRecognizer];
The panning is working okay, but not great. The pinching is the real problem. The idea for the pinching is to grab a point at the center of the screen, convert that point to the world node, and then move to it while zooming in. Here is the method for pinching:
-(void) handlePinch:(UIPinchGestureRecognizer *)sender {
if (sender.state == UIGestureRecognizerStateBegan) {
_tempScale = [sender scale];
}
if (sender.state == UIGestureRecognizerStateChanged) {
if([sender scale] > _tempScale) {
if (_world.xScale < 6) {
//_world.xScale += 0.05;
//_world.yScale += 0.05;
//[_world setScale:[sender scale]];
[_world setScale:_world.xScale += 0.05];
CGPoint screenCenter = CGPointMake(_initialScreenSize.width/2, _initialScreenSize.height/2);
CGPoint newWorldPoint = [self convertTouchPointToWorld:screenCenter];
//crazy method why does this work
CGPoint alteredWorldCenter = CGPointMake(((newWorldPoint.x*_world.xScale)*-1), (newWorldPoint.y*_world.yScale)*-1);
//why does the duration have to be exactly 0.3 to work
SKAction *moveToCenter = [SKAction moveTo:alteredWorldCenter duration:0.3];
[_world runAction:moveToCenter];
}
} else if ([sender scale] < _tempScale) {
if (_world.xScale > 0.5 && _world.xScale > 0.3){
//_world.xScale -= 0.05;
//_world.yScale -= 0.05;
//[_world setScale:[sender scale]];
[_world setScale:_world.xScale -= 0.05];
CGPoint screenCenter = CGPointMake(_initialScreenSize.width/2, _initialScreenSize.height/2);
CGPoint newWorldPoint = [self convertTouchPointToWorld:screenCenter];
//crazy method why does this work
CGPoint alteredWorldCenter = CGPointMake(((newWorldPoint.x*_world.xScale - _initialScreenSize.width)*-1), (newWorldPoint.y*_world.yScale - _initialScreenSize.height)*-1);
SKAction *moveToCenter = [SKAction moveTo:alteredWorldCenter duration:0.3];
[_world runAction:moveToCenter];
}
}
}
if (sender.state == UIGestureRecognizerStateEnded) {
[_world removeAllActions];
}
}
I've tried many iterations of this, but this exact code is what is getting me the closest to pinching on a point in the world. There are some problems though. As you get further out from the center, it doesn't work as well, as it pretty much still tries to zoom in on the very center of the world. After converting the center point to the world node, I still need to manipulate it again to get it centered properly (the formula I describe as crazy). And it has to be different for zooming in and zooming out to work. The duration of the move action has to be set to 0.3 or it pretty much won't work at all. Higher or lower and it doesn't zoom in on the center point. If I try to increment the zoom by more than a small amount, it moves crazy fast. If I don't end the actions when the pinch ends, the screen jerks. I don't understand why this works at all (it smoothly zooms in to the center point before the delay ends and the screen jerks) and I'm not sure what I'm doing wrong. Any help is much appreciated!

Take a look at my answer to a very similar question.
https://stackoverflow.com/a/21947549/3148272
The code I posted "anchors" the zoom at the location of the pinch gesture instead of the center of the screen, but that is easy to change as I tried it both ways.
As requested in the comments below, I am also adding my panning code to this answer.
Panning Code...
// instance variables of MyScene.
SKNode *_mySkNode;
UIPanGestureRecognizer *_panGestureRecognizer;
- (void)didMoveToView:(SKView *)view
{
_panGestureRecognizer = [[UIPanGestureRecognizer alloc] initWithTarget:self action:#selector(handlePanFrom:)];
[[self view] addGestureRecognizer:_panGestureRecognizer];
}
- (void)handlePanFrom:(UIPanGestureRecognizer *)recognizer
{
if (recognizer.state == UIGestureRecognizerStateBegan) {
[recognizer setTranslation:CGPointZero inView:recognizer.view];
} else if (recognizer.state == UIGestureRecognizerStateChanged) {
CGPoint translation = [recognizer translationInView:recognizer.view];
translation = CGPointMake(-translation.x, translation.y);
_mySkNode.position = CGPointSubtract(_mySkNode.position, translation);
[recognizer setTranslation:CGPointZero inView:recognizer.view];
} else if (recognizer.state == UIGestureRecognizerStateEnded) {
// No code needed for panning.
}
}
The following are the two helper functions that were used above. They are from the Ray Wenderlich book on Sprite Kit.
SKT_INLINE CGPoint CGPointAdd(CGPoint point1, CGPoint point2) {
return CGPointMake(point1.x + point2.x, point1.y + point2.y);
}
SKT_INLINE CGPoint CGPointSubtract(CGPoint point1, CGPoint point2) {
return CGPointMake(point1.x - point2.x, point1.y - point2.y);
}

Related

Using gestures with SpriteKit to Drag, rotate and scale multiple sprite separately at the same time

I have created an iOS app in which I need to be able to move, rotate and scale a sprite ( I am using Apple's Sprite Kit) at the same time. For the most part I have this working. I currently can touch with 1 finger and move the sprite, and if I use two fingers I can scale and rotate the sprite. To do this I am using UIPanGestureRecognizer, UIPinchGestureRecognizer and UIRotateGestureRecognizer. That works fine. What I would like is, while I am dragging, rotating and scaling a one sprite with my right hand, I can take my left hand and drag rotate and scale a different sprite independently of the other sprite.
Currently I am using iOS gestures to move, rotate and scale the sprites. I used code very close to what I found on Ray Wenderlich's website in his Drag and Drop Sprites tutorial for Sprite Kit. http://www.raywenderlich.com/44270/sprite-kit-tutorial-how-to-drag-and-drop-sprites. The part I am using is near the bottom when he start to use UIPanGestureRecognizers instead of just the touch method.
Like I said the Gestures work fine on one sprite at a time. How do I make it work on more than one sprite?
For instance for the UIPanGesturRecognizer I add the code below:
- (void)didMoveToView:(SKView *)view {
UIPanGestureRecognizer *gestureRecognizer = [[UIPanGestureRecognizer alloc] initWithTarget:self action:#selector(handlePanFrom:)];
[[self view] addGestureRecognizer:gestureRecognizer];
}
Then I have a method for that called gestureRecognizer below:
- (void)handlePanFrom:(UIPanGestureRecognizer *)recognizer {
if (recognizer.state == UIGestureRecognizerStateBegan) {
CGPoint touchLocation = [recognizer locationInView:recognizer.view];
touchLocation = [self convertPointFromView:touchLocation];
[self selectNodeForTouch:touchLocation]; // This just returns the node that has been touched
} else if (recognizer.state == UIGestureRecognizerStateChanged) {
CGPoint translation = [recognizer translationInView:recognizer.view];
translation = CGPointMake(translation.x, -translation.y);
[self panForTranslation:translation];
[recognizer setTranslation:CGPointZero inView:recognizer.view];
} else if (recognizer.state == UIGestureRecognizerStateEnded) {
[_selectedNode removeAllActions];
}
}
Finally there is the method that moves the sprite:
- (void)panForTranslation:(CGPoint)translation {
if([[_selectedNode name] isEqualToString:kAnimalNodeName]) {
CGPoint position = [_selectedNode position];
// Set variable for the point to move selected node
CGPoint movePoint = CGPointMake(position.x + translation.x, position.y + translation.y);
[_selectedNode setPosition:newPos];
}
}
Now the example code is showing only the methods for the UIPanGestureRecognizer but I also have similar methods for the rotate and pinch gestures. All of this code is in my scene class.
Thank you for the help.
Well, the tutorial you posted pretty much shows how to do it...
-(void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
UITouch *touch = [touches anyObject];
CGPoint positionInScene = [touch locationInNode:self];
[self selectNodeForTouch:positionInScene];
}
- (void)selectNodeForTouch:(CGPoint)touchLocation {
//The below statement assigns touchedNode from a sprite that contains touchLocation
SKSpriteNode *touchedNode = (SKSpriteNode *)[self nodeAtPoint:touchLocation];
//2
if(![_selectedNode isEqual:touchedNode]) {
[_selectedNode removeAllActions];
[_selectedNode runAction:[SKAction rotateToAngle:0.0f duration:0.1]];
_selectedNode = touchedNode;
//the below if statement determines what SKNode is to be given a SKAction
if([[touchedNode name] isEqualToString:kAnimalNodeName]) {
SKAction *sequence = [SKAction sequence:#[[SKAction rotateByAngle:degToRad(-4.0f) duration:0.1],
[SKAction rotateByAngle:0.0 duration:0.1],
[SKAction rotateByAngle:degToRad(4.0f) duration:0.1]]];
[_selectedNode runAction:[SKAction repeatActionForever:sequence]];
}
}
}
So if you want to apply the action to multiple nodes, simply give them the same name. I suggest naming your nodes, put them in an array, and then iterate through them checking if they have the same name.
If you have any further questions please comment.
UPDATE: 1
- (void)panForTranslation:(CGPoint)translation {
//Once again you would do the same thing. Just give the nodes the same name.
if([[_selectedNode name] isEqualToString:kAnimalNodeName]) {
CGPoint position = [_selectedNode position];
// Set variable for the point to move selected nodes
CGPoint movePoint = CGPointMake(position.x + translation.x, position.y + translation.y);
[_selectedNode setPosition:newPos];
}
I'm also using that tutorial to do something similar. The sample code relies on an instance variable, selectedNode, that represents the one node that is selected.
To make this work for multiple nodes, I would recommend using an NSMutableArray of selectedNodes, or subclassing SKSpriteNode to store whether or not it is currently "selected". Good luck!

Zooming an SKNode inconsistent

I have created my own solution for zooming in or out on a specific SKNode without having the zoom the entire scene, and it seems to work mostly how I would expect it to work, with 2 notable exceptions which I am hoping to get input on here. First the code (this control statement is within the touchesMoved method):
if (touches.count == 2) {
// this means there are two fingers on the screen
NSArray *fingers = [touches allObjects];
CGPoint fingOneCurr = [fingers[0] locationInNode:self];
CGPoint fingOnePrev = [fingers[0] previousLocationInNode:self];
CGPoint fingTwoCurr = [fingers[1] locationInNode:self];
CGPoint fingTwoPrev = [fingers[1] previousLocationInNode:self];
BOOL yPinch = fingOneCurr.y > fingOnePrev.y && fingTwoCurr.y < fingTwoPrev.y;
BOOL yUnpinch = fingOneCurr.y < fingOnePrev.y && fingTwoCurr.y > fingTwoPrev.y;
BOOL xPinch = fingOneCurr.x > fingOnePrev.x && fingTwoCurr.x < fingTwoPrev.x;
BOOL xUnpinch = fingOneCurr.x < fingOnePrev.x && fingTwoCurr.x > fingTwoPrev.x;
if (xUnpinch | yUnpinch) {
if (YES) NSLog(#"This means an unpinch is happening");
mapScale = mapScale +.02;
[map setScale:mapScale];
}
if (xPinch | yPinch) {
if (YES) NSLog(#"This means a pinch is happening");
mapScale = mapScale - .02;
[map setScale:mapScale];
}
}
Now the problems:
The pinch and unpinch are not always right sometimes, and I cannot quite put my finger on when this is happening, the pinch will behave as an unpinch and vis a versa.
When the pinching and unpinching is scaling the SKNode correctly, it is rarely as smooth as I would like. There is a bit of jerkiness to it which I find annoying.
Can anyone suggest improvements to this method? Thanks!
This will solve your problem, thanks to Steffen for the hints.
- (void)didMoveToView:(SKView *)view
{
UIPinchGestureRecognizer *precog = [[UIPinchGestureRecognizer alloc] initWithTarget:self action:#selector(handlePinch:)];
[self.scene.view addGestureRecognizer:precog];
}
- (void)handlePinch:(UIPinchGestureRecognizer *) recognizer
{
//NSLog(#"Pinch %f", recognizer.scale);
//[_bg setScale:recognizer.scale];
[_bg runAction:[SKAction scaleBy:recognizer.scale duration:0]];
recognizer.scale = 1;
}

ios - UIPanGestureRecognizer how to get the touched coordinate relative to [gestureRecognizer view]

In my code I need to find the coordinate a user touched and is currently panning.
I've set up the following code based on - Touch Coordinates from UIPanGestureRecognizer
It looks as following:
CGPoint pos = [gestureRecognizer locationInView:[gestureRecognizer view]];
However the values returned by this method are either negative, or larger than the view's own frame. How can that be? What am I doing wrong?
Thanks
Try this one its giving me the coordinates of x and y
-(BOOL)gestureRecognizerShouldBegin:(UIGestureRecognizer *)gestureRecognizer
{
CGPoint touchPoint = [gestureUp locationOfTouch:0 inView:gestureUp.view];
if (touchPoint.y <= 160)
{
if (gestureUp.direction == UISwipeGestureRecognizerDirectionUp)
{
NSLog(#"the location x...%f",touchPoint.x);
NSLog(#"the location y...%f",touchPoint.y);
}
else
{ }
}
return YES;
}

Why does panning let the user move the zoomed view outside the superview?

I am working on pinch in and pinch out feature on pdf pages. My pinch in and panning(moving) is working properly, but when user continuously moves the zoomed view, the zoom view goes outside the super view bounds.Something like this:
how can i limit the pan move so that user could not move the zoomed view/pdf outside the superview.
the relevant code i am using is:
// This method will handle the PINCH / ZOOM gesture
- (void)pinchZoom:(UIPinchGestureRecognizer *)gestureRecognizer
{
if([gestureRecognizer state] == UIGestureRecognizerStateBegan) {
// Reset the last scale, necessary if there are multiple objects with different scales
lastScale = [gestureRecognizer scale];
}
if ([gestureRecognizer state] == UIGestureRecognizerStateBegan || [gestureRecognizer state] == UIGestureRecognizerStateChanged) {
if (!zoomActive) {
zoomActive = YES;
panActive = YES;
UIPanGestureRecognizer *panGesture = [[UIPanGestureRecognizer alloc] initWithTarget:self action:#selector(panMove:)];
[panGesture setMaximumNumberOfTouches:2];
[panGesture setDelegate:self];
[self addGestureRecognizer:panGesture];
[panGesture release];
}
CGFloat currentScale = [[[gestureRecognizer view].layer valueForKeyPath:#"transform.scale"] floatValue];
// Constants to adjust the max/min values of zoom
const CGFloat kMaxScale = 2.0;
const CGFloat kMinScale = 1.0;
CGFloat newScale = 1 - (lastScale - [gestureRecognizer scale]);
newScale = MIN(newScale, kMaxScale / currentScale);
newScale = MAX(newScale, kMinScale / currentScale);
CGAffineTransform transform = CGAffineTransformScale([[gestureRecognizer view] transform], newScale, newScale);
[gestureRecognizer view].transform = transform;
lastScale = [gestureRecognizer scale]; // Store the previous scale factor for the next pinch gesture call
[delegate leavesView:self zoomingCurrentView:[gestureRecognizer scale]];
}
}
the method where i am handling the pan move:
// This method will handle the PAN / MOVE gesture
- (void)panMove:(UIPanGestureRecognizer *)gestureRecognizer
{
if ([gestureRecognizer state] == UIGestureRecognizerStateBegan || [gestureRecognizer state] == UIGestureRecognizerStateChanged) {
CGPoint translation = [gestureRecognizer translationInView:[[gestureRecognizer view] superview]];
[[gestureRecognizer view] setCenter:CGPointMake([[gestureRecognizer view] center].x + translation.x, [[gestureRecognizer view] center].y + translation.y)];
[gestureRecognizer setTranslation:CGPointZero inView:[[gestureRecognizer view] superview]];
}
}
Please suggest how to handle pan/move limiting panning within its superview bounds.
Try this code in you panMove method. Its working fine in my case.
static CGPoint initialCenter;
if (recognizer.state == UIGestureRecognizerStateBegan)
{
initialCenter = recognizer.view.center;
}
CGPoint translation = [recognizer translationInView:recognizer.view];
CGPoint newCenter = CGPointMake(initialCenter.x + translation.x,
initialCenter.y + translation.y);
CGRect newFrame = recognizer.view.frame;
CGRect superViewBounds = recognizer.view.superview.bounds;
CGPoint superViewOrigin = recognizer.view.superview.frame.origin;
if(newCenter.x-(newFrame.size.width/2) >= (superViewBounds.size.width+superViewOrigin.x)-200 /*right*/
||
newCenter.x+(newFrame.size.width/2) <= (superViewOrigin.x+200) /*left*/
||
newCenter.y-(newFrame.size.height/2) >= (superViewBounds.size.height+superViewOrigin.y)-200 /*bottom*/
||
newCenter.y+(newFrame.size.height/2) <= (superViewOrigin.y+100)) /*top*/
{
return;
}else{
recognizer.view.center = newCenter;
}
You can add something like this to the code: (This is a little rough so you may have to work out some errors)
if([gestureRecognizer view].frame.bounds.x < self.view.bounds.x - panExapantion)
{
//then don't move it
}
//... repeat this for all sides (this is left), bottom, right, and top.
Edit:
Ok, consider a box inside of a boxes, thus we have an inside box and an outer box. If we don't want the inside box to go outside the outer box then we must have all these statements be true:
The moved left side of the inside box is not outside the left side of the outer box.
The moved right side of the inside box is not outside the right side of the outer box.
The moved bottom side of the inside box is not outside the bottom side of the outer box.
The moved top side of the inside box is not outside the top side of the outer box.
In your case the PDF is the inside box and the iPad is the outer box. In order to stop the pdf from going outside the box we need to check if each of these statements is true, and if one is false we do not move the PDF to it's new location OR we move the PDF just near the edge of the iPhone screen.
The problem is if the pinch and zoom is used then the suddenly the box will always be outside the outer box, so how do we fix it? We get how much pixels were added to the inside box when it was zoomed (for the sack of this explanation lets just call this the expansion). So we get how much the box was expanded by and subtract that value. Like so: (This is a dumbed down if statement and will not work in code)
If(outerBox.leftSide is less than innerBox.leftSide - panExpantion)
{
//Then the innerBox is outside the outterBox
}
I hoped this helped clarify!

How to detect or define the orientation of a pinch gesture with UIPinchGestureRecognizer?

I'm using UIPinchGestureRecognizer to detect pinch gestures, something like:
- (void) initPinchRecon {
UIPinchGestureRecognizer *pinchRecognizer = [[[UIPinchGestureRecognizer alloc]
initWithTarget:self
action:#selector(Perform_Pinch:)] autorelease];
[self addGestureRecognizer:pinchRecognizer];
[pinchRecognizer setScale:20.0f];
}
- (void) Perform_Pinch:(UIPinchGestureRecognizer*)sender{
NSLog(#"PINCH");
}
And it works well to detect a simple pinch gesture: It's possible to determine (or define myself) the angle or orientation of the pinch gesture ?, for example, to differentiate between an horizontal and an vertical pinch gesture ?
A very simple solution is to implement the gesture handler like this:
-(void)handlePinchGesture:(UIPinchGestureRecognizer *)recognizer {
if (recognizer.state != UIGestureRecognizerStateCancelled) {
if (recognizer.numberOfTouches == 2) {
CGPoint firstPoint = [recognizer locationOfTouch:0 inView:recognizer.view];
CGPoint secondPoint = [recognizer locationOfTouch:1 inView:recognizer.view];
CGFloat angle = atan2(secondPoint.y - firstPoint.y, secondPoint.x - firstPoint.x);
// handle the gesture based on the angle (in radians)
}
}

Resources