I'm having a problem when trying to drag a view using a pan gesture recognizer. The view is a collectionViewCell and the dragging code is working, except when the drag starts the view jumps up and to the left. My code is below.
In the collectionViewCell:
override func awakeFromNib() {
super.awakeFromNib()
let panRecognizer = UIPanGestureRecognizer(target:self, action:#selector(detectPan))
self.gestureRecognizers = [panRecognizer]
}
var firstLocation = CGPoint(x: 0, y: 0)
var lastLocation = CGPoint(x: 0, y: 0)
#objc func detectPan(_ recognizer:UIPanGestureRecognizer) {
switch recognizer.state {
case .began:
firstLocation = recognizer.translation(in: self.superview)
lastLocation = recognizer.translation(in: self.superview)
case .changed:
let translation = recognizer.translation(in: self.superview)
self.center = CGPoint(x: lastLocation.x + translation.x, y: lastLocation.y + translation.y)
default:
UIView.animate(withDuration: 0.1) {
self.center = self.firstLocation
}
}
}
The first image is before the drag starts, the second is what happens when dragging up.
You're using self.center instead of using self.frame.origin.x and self.frame.origin.y then later you're setting your translation and adding it to the lastLocation.
Effectively what's happening is that your view is calculating the position changed from the center of the view, as if you were perfectly dragging from that location and then translating + lastLocation. I'm sure just by reading that you're aware of the issue.
The fix is simple.
self.frame.origin.x = translation.x
self.frame.origin.y = translation.y
The difference is the starting calculation with the translation. Origin will grab the x/y position based on where the touch event begins. Whereas the .center always goes from the center.
Related
I have the storyboard in the below screenshot.
The gray area is a UIViewContainer. UIGestureRegonizers are enabling the zooming and the moving of the UIViewContainer through Pan-Pinch gestures. The location of the buttons are fixed in the bottom with constraints, the UIViewContainer has flexible size and connected to buttons and the safe are with constraints.
So here is the problem: First, I zoom into a part of the UIViewContainer and then, I change the title of the up-right button. Right after that, for some reason that I need help with, the container moves about 10 pixels to the right. I am sure that no lifecycle function is called after the button.setTitle() function. I think it can be the problem that the container does some kind of a relayout.
Here is a dummy app where I reproduced the same behavior. After you move the container to somewhere with a pan gesture and click the button, the button title changes and the view is moved back to center. I want the view to stay where it is.
How can I disable the layout after the button title is changed?
#IBAction func handlePinchGestures(pinchGestureRecognizer: UIPinchGestureRecognizer) {
if let view = pinchGestureRecognizer.view {
switch pinchGestureRecognizer.state {
case .changed:
let pinchCenter = CGPoint(x: pinchGestureRecognizer.location(in: view).x - view.bounds.midX,
y: pinchGestureRecognizer.location(in: view).y - view.bounds.midY)
let transform = view.transform.translatedBy(x: pinchCenter.x, y: pinchCenter.y)
.scaledBy(x: pinchGestureRecognizer.scale, y: pinchGestureRecognizer.scale)
.translatedBy(x: -pinchCenter.x, y: -pinchCenter.y)
view.transform = transform
pinchGestureRecognizer.scale = 1
imageLayerContainer.subviews.forEach({ (subview: UIView) -> Void in subview.setNeedsDisplay() })
default:
return
}
}
}
#IBAction func handlePanGesturesWithTwoFingers(panGestureRecognizer: UIPanGestureRecognizer) {
let translation = panGestureRecognizer.translation(in: self.view)
if let view = panGestureRecognizer.view {
view.center = CGPoint(x:view.center.x + translation.x,
y:view.center.y + translation.y)
}
panGestureRecognizer.setTranslation(CGPoint.zero, in: self.view)
}
I'm developing a card view like in Tinder. When cards X origin is bigger than a value which I declare, It moves out the screen. Otherwise, It sticks to center again. I'm doing all of these things inside UIPanGestureRecognizer function. I can move the view in Change state. However, It sometimes doesn't get into end state so card is neither moves out of the screen or stick to center again. It just stays in some weird place.
So My problem is that card should go out of the screen like in below screenshot or stick into center.
I tried solutions in below post but nothing worked:
UIPanGestureRecognizer not calling End state
UIPanGestureRecognizer does not switch to state "End" or "Cancel" if user panned x and y in negative direction
/// This method handles the swiping gesture on each card and shows the appropriate emoji based on the card's center.
#objc func handleCardPan(sender: UIPanGestureRecognizer) {
// Ensure it's a horizontal drag
let velocity = sender.velocity(in: self.view)
if abs(velocity.y) > abs(velocity.x) {
return
}
// if we're in the process of hiding a card, don't let the user interace with the cards yet
if cardIsHiding { return }
// change this to your discretion - it represents how far the user must pan up or down to change the option
// distance user must pan right or left to trigger an option
let requiredOffsetFromCenter: CGFloat = 80
let panLocationInView = sender.location(in: view)
let panLocationInCard = sender.location(in: cards[0])
switch sender.state {
case .began:
dynamicAnimator.removeAllBehaviors()
let offset = UIOffsetMake(cards[0].bounds.midX, panLocationInCard.y)
// card is attached to center
cardAttachmentBehavior = UIAttachmentBehavior(item: cards[0], offsetFromCenter: offset, attachedToAnchor: panLocationInView)
//dynamicAnimator.addBehavior(cardAttachmentBehavior)
let translation = sender.translation(in: self.view)
print(sender.view!.center.x)
sender.view!.center = CGPoint(x: sender.view!.center.x + translation.x, y: sender.view!.center.y)
sender.setTranslation(CGPoint(x: 0, y: 0), in: self.view)
case .changed:
//cardAttachmentBehavior.anchorPoint = panLocationInView
let translation = sender.translation(in: self.view)
print(sender.view!.center.x)
sender.view!.center = CGPoint(x: sender.view!.center.x + translation.x, y: sender.view!.center.y)
sender.setTranslation(CGPoint(x: 0, y: 0), in: self.view)
case .ended:
dynamicAnimator.removeAllBehaviors()
if !(cards[0].center.x > (self.view.center.x + requiredOffsetFromCenter) || cards[0].center.x < (self.view.center.x - requiredOffsetFromCenter)) {
// snap to center
let snapBehavior = UISnapBehavior(item: cards[0], snapTo: CGPoint(x: self.view.frame.midX, y: self.view.frame.midY + 23))
dynamicAnimator.addBehavior(snapBehavior)
} else {
let velocity = sender.velocity(in: self.view)
let pushBehavior = UIPushBehavior(items: [cards[0]], mode: .instantaneous)
pushBehavior.pushDirection = CGVector(dx: velocity.x/10, dy: velocity.y/10)
pushBehavior.magnitude = 175
dynamicAnimator.addBehavior(pushBehavior)
// spin after throwing
var angular = CGFloat.pi / 2 // angular velocity of spin
let currentAngle: Double = atan2(Double(cards[0].transform.b), Double(cards[0].transform.a))
if currentAngle > 0 {
angular = angular * 1
} else {
angular = angular * -1
}
let itemBehavior = UIDynamicItemBehavior(items: [cards[0]])
itemBehavior.friction = 0.2
itemBehavior.allowsRotation = true
itemBehavior.addAngularVelocity(CGFloat(angular), for: cards[0])
dynamicAnimator.addBehavior(itemBehavior)
showNextCard()
hideFrontCard()
}
default:
break
}
}
I was checking If I'm swiping in horizontal with:
let velocity = sender.velocity(in: self.view)
if abs(velocity.y) > abs(velocity.x) {
return
}
For some reason, It was getting in to return while I'm swiping horizontal. When I comment this block of code, everything started to work :)
So I have a UILabel on a UIView nib file and I am trying to give the user the ability to drag the label around and when they stop dragging the label stays there and if they try to drag it again they can.
I am able to drag the Label just fine using the UIPanGestureRecognizer and when I stop dragging it the label stays where it is. The problem I am having is that when I attempt to drag it again the Label jumps back to the original starting position at the center of the UIView.
I understand that using the UIPanGestureRecognizer.translation(in: UIVIew) gives the original coordinates as (x: 0.0, y: 0.0) when the actual coordinates that the Label starts at are (x: 150.0, y: 90.0).
#IBOutlet var cardView: UIView!
#IBOutlet weak var testLBL: UILabel!
override func awakeFromNib() {
super.awakeFromNib()
// trying to move the label
let gesture = UIPanGestureRecognizer(target: self, action: #selector(self.wasDragged(gestureRecognizer:)))
testLBL.isUserInteractionEnabled = true
cardView.clipsToBounds = true
testLBL.addGestureRecognizer(gesture)
}
Below is the function that is used to handle the action, I have several other things I have tried commented out. I am also printing the current coordinates of the Label within the cardView
// function that helps drag the label around
func wasDragged(gestureRecognizer: UIPanGestureRecognizer) {
let translation = gestureRecognizer.translation(in: cardView)
//print(translation)
// currently taking it from the original point each time you try to drag it
testLBL.center = CGPoint(x: self.bounds.width / 2 + translation.x, y: self.bounds.height / 2 + translation.y)
//how you will know what position the label was moved to
print(["x",self.testLBL.frame.origin.x,"y", self.testLBL.frame.origin.y])
//let newTranslation = gestureRecognizer.translation(in: cardView)
//var coordinates = CGPoint(x: self.testLBL.frame.origin.x, y: self.testLBL.frame.origin.y)
}
The testLBL.center = CGPoint(x: self.bounds.width / 2 + translation.x, y: self.bounds.height / 2 + translation.y) operation is what allows the label to move and works find I just want it to not jump back to the middle of the UIView when I try to drag it again.
Any help is greatly appreciated because I am still pretty new at coding, Thank You!
// function that helps drag the label around
#objc
func wasDragged(gestureRecognizer: UIPanGestureRecognizer) {
let translation = gestureRecognizer.translation(in: self.view)
let selectedLabel = gestureRecognizer.view!
selectedLabel.center = CGPoint(x: selectedLabel.center.x + translation.x,
y: selectedLabel.center.y + translation.y)
gestureRecognizer.setTranslation(CGPoint.zero, in: self.view)
print(["1 x",self.testLbl.frame.origin.x,"y", self.testLbl.frame.origin.y])
}
Tested this code and it works, hope it helps anyone!
this might help
CGPoint translation = [recognizer translationInView:self.superview];
testLBL.center = CGPointMake(self.center.x + translation.x, self.center.y + translation.y);
[recognizer setTranslation:CGPointZero inView:self];
apply it on swift you won't find that hard
I have a demo app for testing physics in SpriteKit. I was able to simulate inertia after throwing sprite with pan gesture. When I release finger bouncing works great.
Problem is: How to simulate bouncing during dragging? I would like to red SKSpriteNode bounce off when I hit it with dragging SKSpriteNode.
I tried to set self.selectedNode?.physicsBody?.isDynamic = true but then sprite moves away under finger even when I set correct position.
Here is my repo. Feel free to copy or experiment.
Crucial code:
func handlePan(panGestureRecognizer recognizer:UIPanGestureRecognizer) {
let touchLocationView = recognizer.location(in: recognizer.view)
let touchLocationScene = self.convertPoint(fromView: touchLocationView)
switch recognizer.state {
case .began:
self.showMoved = false
let canditateNode = self.touchedNode(touchLocationScene)
if let name = canditateNode.name, name.contains(kMovableNode) {
self.selectedNode = canditateNode
self.selectedNode?.physicsBody?.isDynamic = false;
}
case .changed:
let translation = recognizer.translation(in: recognizer.view)
if let position = self.selectedNode?.position {
self.selectedNode?.position = CGPoint(x: position.x + translation.x, y: position.y - translation.y)
recognizer.setTranslation(CGPoint.zero, in: recognizer.view)
}
case .ended:
self.selectedNode?.physicsBody?.isDynamic = true;
let velocity = recognizer.velocity(in: recognizer.view)
self.selectedNode?.physicsBody?.applyImpulse(CGVector(dx: velocity.x, dy: -velocity.y))
self.selectedNode = nil
self.showMoved = true
default:
break
}
}
I have one imageview and a textfield on that image. I make the textfield draggable with below code but it can be draggable to anywhere in the screen. I want that textfield draggable only in limit of imageview. If I uncomment if check in the draagedView function, textfield stuck at the left side of imageview because their x values become same.
I found this solution but can't modify it to work on my project.
Use UIPanGestureRecognizer to drag UIView inside limited area
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
let gesture = UIPanGestureRecognizer(target: self, action: #selector(ViewController.draggedView(_:)))
bottomTextField.addGestureRecognizer(gesture)
bottomTextField.isUserInteractionEnabled = true
}
func userDragged(gesture: UIPanGestureRecognizer){
let loc = gesture.location(in: self.view)
self.bottomTextField.center = loc
}
func draggedView(_ sender:UIPanGestureRecognizer) {
let compare = MyimageView.frame.maxX <= bottomTextField.frame.maxX
//if(MyimageView.frame.minX <= bottomTextField.frame.minX && compare )
// {
self.view.bringSubview(toFront: sender.view!)
let translation = sender.translation(in: self.view)
sender.view!.center = CGPoint(x: sender.view!.center.x + translation.x, y: sender.view!.center.y + translation.y)
sender.setTranslation(CGPoint.zero, in: self.view)
// }
}
The main problem is that you are checking the position before you do the translation. That means that the text field ends up in an invalid position, after which the if block is never reached.
This is a slightly different way to approach the problem and means that the text field reaches right to the edges of the limit:
func draggedView(_ sender: UIPanGestureRecognizer) {
guard let senderView = sender.view else {
return
}
var translation = sender.translation(in: view)
translation.x = max(translation.x, MyimageView.frame.minX - bottomTextField.frame.minX)
translation.x = min(translation.x, MyimageView.frame.maxX - bottomTextField.frame.maxX)
translation.y = max(translation.y, MyimageView.frame.minY - bottomTextField.frame.minY)
translation.y = min(translation.y, MyimageView.frame.maxY - bottomTextField.frame.maxY)
senderView.center = CGPoint(x: senderView.center.x + translation.x, y: senderView.center.y + translation.y)
sender.setTranslation(.zero, in: view)
view.bringSubview(toFront: senderView)
}
I have also made it a bit safer by adding a guard statement at the top and removing the force unwraps.