I've used the following code, shown below, to implement drag & drop in several different places and has always worked well for me in the past.
Now I have a problem with it and have no idea why. It works perfectly so long as I have the device (or simulator) oriented portrait with the button down. But in any of the other three orientations, as the finger dragging the view moves one direction, the "dragged" view moves a different direction.
As shown below I'm logging the value of translation with each move. In the original orientation, as I drag from the middle of the screen toward the lower-left corner, the values for translation are:
-, +
If I rotate left, and do it again:
-, -
Rotate left again:
+, -
Rotate left again:
+, +
I am totally not getting what happens here, particularly since this code seems to work well in other view controllers.
Any suggestions will be appreciated.
- (void) didMakePanGesture:(UIPanGestureRecognizer *)panGesture
{
if (panGesture.state == UIGestureRecognizerStateBegan)
{
[self setDropTargetsCoordinates]; // saves correct drop target & its coordinates
dragViewStartLocation = receptiveClassificationImageView.center; // save center in case we have to snap back
receptiveClassificationImageView.transform = CGAffineTransformMakeScale(0.40f, 0.40f); // make the image smaller for dragging
receptiveClassificationImageView.layer.cornerRadius = 12.0f; // we lose the rounded corners in the scaling; this to fix
}
else if (panGesture.state == UIGestureRecognizerStateChanged)
{
//
// Adjust the location of the dragged view whenever state changes
//
CGPoint translation = [panGesture translationInView:nil];
CGAffineTransform transform = receptiveClassificationImageView.transform;
transform.tx = translation.x;
transform.ty = translation.y;
receptiveClassificationImageView.transform = transform;
NSLog(#"Translation=%f,%f" translation.x, translation.y);
}
else if (panGesture.state == UIGestureRecognizerStateEnded)
{
// do stuff when dropped
}
Just in case someone sees this later, this problem was solved with the following changes to the code above:
if (panGesture.state == UIGestureRecognizerStateBegan)
{
[self setDropTargetsCoordinates]; // saves correct drop target & its coordinates
dragViewStartLocation = receptiveClassificationImageView.center; // save center in case we have to snap back
**receptiveClassificationImageView.center = [panGesture locationInView:receptiveClassificationImageView.superview]; // re-center the view before scaling**
receptiveClassificationImageView.transform = CGAffineTransformMakeScale(0.40f, 0.40f); // make the image smaller for dragging
receptiveClassificationImageView.layer.cornerRadius = 12.0f; // we lose the rounded corners in the scaling; this to fix
}
else if (panGesture.state == UIGestureRecognizerStateChanged)
{
//
// Adjust the location of the dragged view whenever state changes
//
CGPoint translation = [panGesture translationInView:**self.view**];
CGAffineTransform transform = receptiveClassificationImageView.transform;
transform.tx = translation.x;
transform.ty = translation.y;
receptiveClassificationImageView.transform = transform;
}
Related
I'm following Ray Wenderlich's 'iOS Games by Tutorials' & I got everything in my world setup & working: The entire game is in Landscape mode & there's one SKNode called _worldNode & everything, except _uiNode (in-game UI), is added to it. Player character walks to a touched location & _worldNode moves under him like a treadmill. However, like all functionality (or as they call it: "juice") addicts I wanted to add zoom in/out functionality through UIPinchGestureRecognizer by scaling _worldNode, which I did. But now every time I zoom in, the "camera" moves to the bottom left. Zooming out moves the view to the top right of the screen. It's a mess. I need the view to stay centered on the player character & I've tried everything I could come up with & find online. The closest thing I came to was using the technique from SKNode scale from the touched point but I still get the bloody bottom left/top right mess. I realized this mess happens only when I update the camera/view (it's really _worldNode.position). Therefore, 'didSimulatePhysics' or 'didFinishUpdate' methods don't help. In fact, even a one time button that slightly moves/updates the camera view (_worldNode.position) still gives me the bottom left/top right problem. Here is my code. I hope someone can take a look & tell me what to modify to get things working.
#interface GameScene () <SKPhysicsContactDelegate, UIGestureRecognizerDelegate>
{
UIPinchGestureRecognizer *pinchGestureRecognizer;
}
//Properties of my GameScene.
#property SKNode *worldNode;
#property etc. etc.
//Called by -(id)initWithSize:(CGSize)size method & creates the in-game world.
-(void)createWorld
{
[_worldNode addChild:_backgroundLayer];
[self addChild:_worldNode];
self.anchorPoint = CGPointMake(0.5, 0.5); //RW tutorial did it this way.
_worldNode.position = CGPointMake(-_backgroundLayer.layerSize.width/2, -_backgroundLayer.layerSize.height/2); //Center.
//Then I add every node to _worldNode from this point on.
}
//Neccessary for gesture recognizers.
-(void)didMoveToView:(SKView *)view
{
pinchGestureRecognizer = [[UIPinchGestureRecognizer alloc] initWithTarget:self action:#selector(handleZoomFrom:)];
[view addGestureRecognizer:pinchGestureRecognizer];
}
//"Camera" follows player character. 'didSimulatePhysics' doesn't help either.
-(void)didFinishUpdate
{
//IF '_pinchingScreen' == YES then screen pinching is in progress. Thus, camera position update will seize for the duration of pinching.
if (!_pinchingScreen)
{
_worldNode.position = [self pointToCenterViewOn:_player.position];
}
}
//Method that is called by my UIPinchGestureRecognizer. Answer from: https://stackoverflow.com/questions/21900614/sknode-scale-from-the-touched-point?lq=1
-(void)handleZoomFrom:(UIPinchGestureRecognizer*)recognizer
{
CGPoint anchorPoint = [recognizer locationInView:recognizer.view];
anchorPoint = [self convertPointFromView:anchorPoint];
if (recognizer.state == UIGestureRecognizerStateBegan)
{
// No code needed for zooming...
_player.movementMode = 2; //Stop character from moving from touches.
_pinchingScreen = YES; //Notifies 'didFinishUpdate' method that pinching began & camera position update should stop for now.
}
else if (recognizer.state == UIGestureRecognizerStateChanged)
{
//Technique from the above Stack Overflow link - Commented out.
// CGPoint anchorPointInMySkNode = [_worldNode convertPoint:anchorPoint fromNode:self];
//
// [_worldNode setScale:(_worldNode.xScale * recognizer.scale)];
//
// CGPoint mySkNodeAnchorPointInScene = [self convertPoint:anchorPointInMySkNode fromNode:_worldNode];
// CGPoint translationOfAnchorInScene = CGPointSubtract(anchorPoint, mySkNodeAnchorPointInScene);
//
// _worldNode.position = CGPointAdd(_worldNode.position, translationOfAnchorInScene);
//
// recognizer.scale = 1.0;
//Modified scale: 2.0
if(recognizer.scale > _previousWorldScale)
{
_previousWorldScale = recognizer.scale;
CGPoint anchorPointInMySkNode = [_worldNode convertPoint:anchorPoint fromNode:self];
[_worldNode setScale:2.0];
CGPoint worldNodeAnchorPointInScene = [self convertPoint:anchorPointInMySkNode fromNode:_worldNode];
CGPoint translationOfAnchorInScene = CGPointSubtract(anchorPoint, worldNodeAnchorPointInScene);
_worldNode.position = CGPointAdd(_worldNode.position, translationOfAnchorInScene);
//[_worldNode runAction:[SKAction scaleTo:2.0 duration:0]]; //This works too.
}
//Original scale: 1.0
if(recognizer.scale < _previousWorldScale)
{
_previousWorldScale = recognizer.scale;
CGPoint anchorPointInMySkNode = [_worldNode convertPoint:anchorPoint fromNode:self];
[_worldNode setScale:1.0];
CGPoint worldNodeAnchorPointInScene = [self convertPoint:anchorPointInMySkNode fromNode:_worldNode];
CGPoint translationOfAnchorInScene = CGPointSubtract(anchorPoint, worldNodeAnchorPointInScene);
_worldNode.position = CGPointAdd(_worldNode.position, translationOfAnchorInScene);
//[_worldNode runAction:[SKAction scaleTo:1.0 duration:0]]; //This works too.
}
}
else if (recognizer.state == UIGestureRecognizerStateEnded)
{
// No code needed here for zooming...
_pinchingScreen = NO; //Notifies 'didFinishUpdate' method that pinching has stopped & camera position update should resume.
_player.movementMode = 0; //Resume character movement.
}
}
So could anyone please tell me, by looking at the above code, why the camera/view shifts to the bottom left upon zooming in? I've sat several days on this problem & I still can't figure it out.
Thanks to JKallio, who wrote detailed code in his answer to Zoom and Scroll SKNode in SpriteKit, I've been able to find a piece of code that solves the problem. There's a method called 'centerOnNode' that is small, elegant & solves my problem perfectly. Here it is for anyone that just needs that:
-(void) centerOnNode:(SKNode*)node
{
CGPoint posInScene = [node.scene convertPoint:node.position fromNode:node.parent];
node.parent.position = CGPointMake(node.parent.position.x - posInScene.x, node.parent.position.y - posInScene.y);
}
Then you call that method inside your 'didSimulatePhysics' or inside 'didFinishUpdate' like so:
//"Camera" follows player character.
-(void)didFinishUpdate
{
//IF '_pinchingScreen' == YES then screen pinching is in progress. Thus, camera position update will seize for the duration of pinching.
if (!_pinchingScreen)
{
if (_previousWorldScale > 1.0) //If _worldNode scale is greater than 1.0
{
[self centerOnNode:_player]; //THIS IS THE METHOD THAT SOLVES THE PROBLEM!
}
else if (_previousWorldScale == 1.0) //Standard _worldNode scale: 1.0
{
_worldNode.position = [self pointToCenterViewOn:_player.position];
}
}
}
P.S. Just remember the question wasn't HOW to zoom in. But how to fix the issue with the camera once the world is ALREADY zoomed in.
So I'm trying to make an app where you pick up objects with your finger, it moves with your finger and then you flick it away to throw it. I found some threads for similar games but not with this particular problem.
Since I couldn't really keep the object along with my finger by applying force or impulse I disable the physics on it while I'm "holding it", measure velocity so when I release it I apply that vector as an impulse.
This works excellent visually but when I try to read the position of the object it's not synced up with "reality". I've made a condition so if velocity isn't strong enough when you release the object it is supposed to return to its starting position but it doesn't move at all (except if you manage to throw it, that works fine) so when I tried to output the .position to the console it didn't correlate to what I was seeing on the screen.
Any ideas? I know you shouldn't set position and simulate physics at the same time but I disable the physics for when I'm dragging the object. Why won't it keep track of .position just because it is moving it with physics?
Here I try to throw a potion:
- (void)handlePan:(UIPanGestureRecognizer *)recognizer {
if (recognizer.state == UIGestureRecognizerStateBegan) { //Picked up
potion.physicsBody.dynamic = NO;
potion.physicsBody.velocity = CGVectorMake(0.0, 0.0);
potion.physicsBody.angularVelocity = 0.0;
potion.zRotation = 0.0;
CGPoint touchLocation = [recognizer locationInView:recognizer.view];
touchLocation = [self convertPointFromView:touchLocation];
[self selectNodeForTouch:touchLocation];
} else if (recognizer.state == UIGestureRecognizerStateChanged) { //Moved around
CGPoint translation = [recognizer translationInView:recognizer.view];
translation = CGPointMake(translation.x, -translation.y);
[self panForTranslation:translation];
[recognizer setTranslation:CGPointZero inView:recognizer.view];
velocity = CGVectorMake(translation.x, translation.y);
} else if (recognizer.state == UIGestureRecognizerStateEnded) { //Dropped
_selectedNode = nil;
potion.physicsBody.dynamic = YES;
if (potion.position.y < 250 && velocity.dy < 20) { //Returned to starting position
NSLog(#"Pos:%f,%f",potion.position.x,potion.position.y);
potion.physicsBody.velocity = CGVectorMake(0.0, 0.0);
potion.physicsBody.angularVelocity = 0.0;
potion.zRotation = 0.0;
potion.position = CGPointMake(gameWidth / 2, 150);
NSLog(#"Returning potion");
NSLog(#"Pos:%f,%f",potion.position.x,potion.position.y);
} else { //Flung away
[potion.physicsBody applyImpulse:velocity];
NSLog(#"Add impulse: %f, %f", velocity.dx, velocity.dy);
NSLog(#"Pos:%f,%f",potion.position.x,potion.position.y);
}
}
}
EDIT:After some more testing I've observed that if it has never been thrown (dynamic with impulse) it always shows (160, 150) as its position no matter where it is. Sometimes it shows something like 160.00031 and I don't know why. After it has been thrown it shows the correct position in the NSLog but changing the position still doesn't work
EDIT2: Just saw that it doesn't show the correct position after a throw. Just different but they seem offset. I throw objects in the +y direction (up) and when I put it down in the left corner the position.x is correct but position.y is offset by about 300. So where y should be 0 it's 300.
I have a pan gesture recogniser acting as a slider, code as follows:
- (void)minutePan:(UIPanGestureRecognizer *)gesture
{
if ((gesture.state == UIGestureRecognizerStateChanged) ||
(gesture.state == UIGestureRecognizerStateEnded)){
CGPoint translation = [gesture translationInView:gesture.view.superview];
float leftBound = gesture.view.bounds.size.width / 2.0f;
float rightBound = gesture.view.bounds.size.width + (leftBound - 60);
float position;
if(gesture.view.center.x < leftBound){
position = leftBound;
}else if(gesture.view.center.x > rightBound){
position = rightBound;
}else{
position = gesture.view.center.x + translation.x;
}
gesture.view.center = CGPointMake(position, gesture.view.center.y);
[gesture setTranslation:CGPointZero inView:gesture.view.superview];
}
}
I have a UIView with the gesture recogniser attached inside another UIView acting as the boundary for it. Its working fine except when I get to the left and right edges. The position variable should not get set lower than half the panned views width but it seems to be updating the view on the screen before the code runs. This causes the view to go outside the bounds and flicker back as long as the user is dragging. Any help would be greatly appreciated thanks.
I want to perform a flip animation when the user drags the finger from the right side of the screen. The state of the animation should be influenced by the length of the drag and shouldn't work automatically.
I used something like this:
if (transitionBegan) {
flipTransition = CATransform3DIdentity;
flipTransition.m34 = 1.0 / -500;
flipTransition = CATransform3DRotate(flipTransition, degree * M_PI / 180.0f,0.0f, 1.0f, 0.0f);
self.view.layer.transform = flipTransition;
}
But now I don't know how to realize the transition between my views, so that view A disappears and view B appears.
Can you help me?
You can write a gesture recognizer that does the transform for you. For example, assuming you're able to flip both left and right, you could do something like the following. But the idea is that you transform the current view if you're less than half way through the flip, and transform the next view if more than half way through it:
- (void)handlePan:(UIPanGestureRecognizer *)gesture
{
static UIView *currentView;
static UIView *previousView;
static UIView *nextView;
if (gesture.state == UIGestureRecognizerStateBegan)
{
// Set the three view variables here, based upon the logic of your app.
// If there is no `previousView` or `nextView`, then set them to `nil`
// as appropriate.
// I happen to be choosing views for child view controllers for my
// custom container, but I'll spare you that in case you're not using
// custom container controller.
}
// lets set the "percent" rotated as the percent across the screen the user's
// finger has travelled
CGPoint translation = [gesture translationInView:gesture.view.superview];
CGFloat percent = translation.x / gesture.view.frame.size.width;
CGFloat rotationPercent = percent;
// let's use the var to keep track of which view will be rotated
UIView *viewToTransform = nil;
if (percent < -0.5 && nextView)
{
// if user has moved finger more than half way across the screen to
// the left, and there is a `nextView`, then we're showing the second
// half of flip to the next screen
currentView.hidden = YES;
nextView.hidden = NO;
previousView.hidden = YES;
rotationPercent += 1.0;
viewToTransform = nextView;
}
else if (percent > 0.5 && previousView)
{
// if user has moved finger more than half way across the screen to
// the right, and there is a `previousView`, then we're showing the second
// half of flip to the previous screen
currentView.hidden = YES;
nextView.hidden = YES;
previousView.hidden = NO;
rotationPercent -= 1.0;
viewToTransform = previousView;
}
else if ((percent < 0 && nextView) || (percent > 0 && previousView))
{
// otherwise we're in the first half of the flip animation, so we're
// showing the `currentView`
currentView.hidden = NO;
nextView.hidden = YES;
previousView.hidden = YES;
viewToTransform = currentView;
}
// do the flip `transform`
CATransform3D transform = CATransform3DIdentity;
transform.m34 = 1.0 / -800;
viewToTransform.layer.transform = CATransform3DRotate(transform, M_PI * rotationPercent, 0.0, 1.0, 0.0);
// if we're all done, let's animate the completion (or if we didn't move far enough,
// the reversal) of the pan gesture
if (gesture.state == UIGestureRecognizerStateEnded ||
gesture.state == UIGestureRecognizerStateCancelled ||
gesture.state == UIGestureRecognizerStateFailed)
{
// I'm personally using an index of my custom container child views, so I'm
// just updating my index appropriately; your logic may obviously differ
// here.
if (percent < -0.5 && nextView)
self.currentChildIndex++;
else if (percent > 0.5 && previousView)
self.currentChildIndex--;
// and animate the completion of the flip animation
[UIView animateWithDuration:0.25
delay:0.0
options:UIViewAnimationOptionCurveEaseInOut
animations:^{
previousView.transform = CGAffineTransformIdentity;
currentView.transform = CGAffineTransformIdentity;
nextView.transform = CGAffineTransformIdentity;
}
completion:NULL];
}
}
You should be able to do it with the help of a UIPanGestureRecognizer (Gesture recognizer that is listening on finger dragging), you'll be able to get the length of the Pan, and from there, calculate your CATransform3D-based translations and scalings following the progress of the panning.
(Built-in animations are not useful there, you have to make some use of CoreAnimation here, it's fun, I can tell you ;-))
Use UIView animateWithDuration: animations: completion: method to animate the current view up to the transition point then in the completion block remove the current view, add the new view and start another similar animation on the new view.
I am building an iPad app where I needed to allow resizing views functionality using divider view provided between two views. The divider view is just a 20px height view between two half screen content views - please refer attached images.
When user scrolls this divider view up or down, both content views changes their sizes appropriately. I have extended UIView and implemented this using touchMoved delegate as code given below in touchesMoved delegate. It works fine. The only thing is missing with TouchMoved is you can't flick divider view to top or bottom directly. You have to drag all the way to top or bottom!
To support flicking the view I have tried UIPanGestureRecognizer but I don't see smooth dragging with it. Setting split-position of parent resizes both content views as shown in the code. When I handle split position change in UIGestureRecognizerStateChanged state, just touching divider view flick it to top or bottom. Handling split position change in UIGestureRecognizerStateEnded does the same but I don't see content view resizing with dividerview scrolling!
Could someone please tell me how could I achieve both smooth scrolling of divider view with resizing content views (like touchMoved) and flicking the view? Any alternative approach would also be fine. Thanks.
- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event {
UITouch *touch = [touches anyObject];
if (touch) {
CGPoint lastPt = [touch previousLocationInView:self];
CGPoint pt = [touch locationInView:self];
float offset = pt.y - lastPt.y;
self.parentViewController.splitPosition = self.parentViewController.splitPosition + offset;
}
}
- (void)handlePan:(UIPanGestureRecognizer*)recognizer {
CGPoint translation = [recognizer translationInView:recognizer.view];
CGPoint velocity = [recognizer velocityInView:recognizer.view];
if (recognizer.state == UIGestureRecognizerStateBegan) {
} else if (recognizer.state == UIGestureRecognizerStateChanged) {
// If I change split position here, I don't see smooth scrolling dividerview...it directly jumps to the top or bottom!
self.parentViewController.splitPosition = self.parentViewController.splitPosition + translation.y;
} else if (recognizer.state == UIGestureRecognizerStateEnded) {
// If I change split position here, the same thing happens at end and I don't see my divider view moving with my scrolling and resizing my views.
self.parentViewController.splitPosition = self.parentViewController.splitPosition + translation.y;
}
}
Initial screen
Increased top view size by scrolling divider view towards bottom.
Top view is totally hidden here but I have to scroll divider view all
the way to top. I want to flick the divider view so that it directly
goes from any position to top
If I understand you correctly, you have two separate problems here.
1st: The 'unsmooth' dragging when using the GestureRecognizer. The problem in your code is, that you add the translation every time the GR calls it's handle method. Since the translation is measured continuously, you're adding a higher value every time it's called (i.e. the first call adds 10 to the split position, the 2nd 20, the 3rd 30 and so on).
To solve this you should set the translation to zero after you've updated the split position:
- (void)handlePan:(UIPanGestureRecognizer*)recognizer {
CGPoint translation = [recognizer translationInView:recognizer.view];
if (recognizer.state == UIGestureRecognizerStateChanged) {
self.parentViewController.splitPosition = self.parentViewController.splitPosition + translation.y;
}
[recognizer setTranslation:CGPointZero inView:recognizer.view];
}
2nd: To allow the user to flick the split position to the top or bottom, you could check the velocity in UIGestureRecognizerStateEnded, and if it exeeds some value instantly set the splitposition to top or bottom.
But it might be more convenient to use a UISwipeGestureRecognizer.
- (void)handleSwipe:(UISwipeGestureRecognizer *)gr
{
if (gr.direction == UISwipeGestureRecognizerDirectionUp){
[self.parentViewController moveSplitViewToTop];
}
else if (gr.direction == UISwipeGestureRecognizerDirectionDown){
[self.parentViewController moveSplitViewToBottom];
}
}
In this case it might be necessary (not sure, maybe it's not) to require the SwipeGR to fail before the PanGR triggers by setting:
[panGestureRecognizer requireGestureRecognizerToFail:swipeGestureRecognizer];
Hope that helped.
Edit:
Regarding your comment, I assume by frequency you mean velocity. If you only use the PanGR you could modify the code above to sth. like this:
- (void)handlePan:(UIPanGestureRecognizer*)recognizer {
CGPoint translation = [recognizer translationInView:recognizer.view];
CGPoint velocity = [recognizer velocityInView:recognizer.view];
if (recognizer.state == UIGestureRecognizerStateChanged) {
self.parentViewController.splitPosition = self.parentViewController.splitPosition + translation.y;
}
else if (recognizer.state == UIGestureRecognizerStateEnded){
if (velocity.y > someValue){
[self.parentViewController moveSplitViewToBottom];
}
else if (velocity.y < someValue){
[self.parentViewController moveSplitViewToTop];
}
}
[recognizer setTranslation:CGPointZero inView:recognizer.view];
}
I'm not sure if the velocity is signed, if not you've got to check for the translation again in the if-block.