How can a function be called immediately after a pan gesture? - ios

Please know that I am new to Swift and IOS. I am using a pan gesture to move a label. When it is finished, I want to automatically add a new label. I have tried to call a function at the end of the pan function, but the label that I moved gets moved off the screen, when the new label is added. I have attempted to use "panGR.state.rawValue == 3", "panGR.state == UIGestureRecognizerState.ended", and a timer - without success. Please help.
Hopefully this is the code that is needed to explain the problem:
func initGestureRecognizers() {
let panGR = UIPanGestureRecognizer(target: self, action:
#selector(ShapeView.didPan(_:)))
addGestureRecognizer(panGR)
}
func didPan(_ panGR: UIPanGestureRecognizer) {
self.superview!.bringSubview(toFront: self)
var size: CGFloat = 53
var translation = panGR.translation(in: self)
translation = translation.applying(self.transform)
self.center.x += translation.x
self.center.y += translation.y
panGR.setTranslation(CGPoint.zero, in: self)
if panGR.state.rawValue == 1 { // save the last position
let startX = self.center.x
let startY = self.center.y
}
if panGR.state.rawValue == 3 { // when move is finished
// this will remove ability to move tile
removeGestureRecognizer(panGR)
// the following code should replace the moved tile,
// with a new tile, but leave the moved tile where it is.
// BUT - it moves the moved tile to coordinates (x=0,y=0), if
// the following 2 lines are executed.
self.center.x = 0
self.center.y = 0
// if the above 2 lines are not executed, the new tile
// does not get displayed
replaceMovedTile()
}
}
func replaceMovedTile() {
// position new tile
let tapPoint = CGPoint(x: 46 + 22, y: 596 + 22)
let shapeView = ShapeView(origin: tapPoint)
addSubview(shapeView)
}

addSubview is a function created for you?
If not, put self.view before
self.view.addSubview(shapeView)

Related

Pan Gesture making view jump away from finger on drag

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.

Stop UIDynamicAnimator Whit view Limits

I have been trying to create a custom UIClass that implements an horizontal scrolling for its inner content by using UIPanGestureRecognizer.
My first success approach was:
#IBAction func dragAction(_ sender: UIPanGestureRecognizer) {
let velocity = sender.velocity(in: sender.view)
if velocity.x > 0{
//print("\(logClassName) Dragging Right ...")
if innerView!.frame.origin.x < CGFloat(0){
offSetTotal += 1
innerView?.transform = CGAffineTransform(translationX: CGFloat(offSetTotal), y: 0)
}
}
else{
//print("\(logClassName) Dragging Left ...")
if totalWidth - innerView!.bounds.maxX < innerView!.frame.origin.x{
offSetTotal -= 1
innerView?.transform = CGAffineTransform(translationX: CGFloat(offSetTotal), y: 0)
}
}
}
That way, althought is a little bit clunky, I am able to scroll from left to right (and reverse) and the innerview is always covering in the holderView.
Then, and because i wanted to get the scrolling effect that you would get with a UIScrolling view i tried the following approach
#objc func handleTap(_ pan: UIPanGestureRecognizer){
let translation = pan.translation(in: innerView)
let recogView = pan.view
let curX = recogView?.frame.origin.x
if pan.state == UIGestureRecognizerState.began {
self.animator.removeAllBehaviors()
recogView?.frame.origin.x = curX! + translation.x
pan.setTranslation(CGPoint.init(x: 0, y: 0), in: recogView)
}else if pan.state == UIGestureRecognizerState.changed {
recogView?.frame.origin.x = curX! + translation.x
pan.setTranslation(CGPoint.init(x: 0, y: 0), in: recogView)
}else if pan.state == UIGestureRecognizerState.ended {
let itemBehavior = UIDynamicItemBehavior(items: [innerView!]);
var velocity = pan.velocity(in: self)
print(velocity.y)
velocity.y = 0
itemBehavior.addLinearVelocity(velocity, for: innerView!);
itemBehavior.friction = 1
itemBehavior.resistance = 1;
//itemBehavior.elasticity = 0.8;
self.collision = UICollisionBehavior(items: [innerView!]);
self.collision!.collisionMode = .boundaries
self.animator.addBehavior(collision!)
self.animator.addBehavior(itemBehavior);
}
Now the problem i face is that it moves horizontally but it goes away and gets lose.
Is that the right approach? Is there a delegate?
The question was posted due to a lack of knowledge of UIScrollViews. I Knew how to do with Storyboard and only scroll vertically.
I ended up creating a programatically UIScrollView and setting up its content size with the inner view.
scrollView = UIScrollView(frame: bounds)
scrollView?.backgroundColor = UIColor.yellow
scrollView?.contentSize = innerView!.bounds.size
scrollView?.addSubview(innerView!)
addSubview(scrollView!)
But first I have defined the innerView.

Get distance from the edge in swift 3

I've got a moveable image using UIPanGestureRecognizer and i need to make the image more transparent the closer it get's to the edge of the screen.
Below is the code I'm using to move and rotate the image the further it get's away from the center.
Adding the UIPanGestureRecognizer to the UIView the image is in:
let moveImage = UIPanGestureRecognizer(target: self, action: #selector(self.detectPan))
moveImage.cancelsTouchesInView = false
MainImageView.addGestureRecognizer(moveImage)
The function that get's called when the UIPanGestureRecognizer starts.
func detectPan(gesture: UIPanGestureRecognizer) {
if gesture.state == UIGestureRecognizerState.began || gesture.state == UIGestureRecognizerState.changed {
let translation = gesture.translation(in: self.view)
gesture.view!.center = CGPoint(x: gesture.view!.center.x + translation.x, y: gesture.view!.center.y)
gesture.setTranslation(CGPoint(x: 0,y: 0), in: self.view)
let newValue = CGFloat(((gesture.view!.center.x + translation.x) - (self.view.bounds.width * 0.50)) / 500)
MainImageView.transform = MainImageView.transform.rotated(by: -lastValue)
MainImageView.transform = MainImageView.transform.rotated(by: newValue)
lastValue = newValue
}
}
I'm assuming that " further it get's away from the center" is relative to the center of the view's parent (or if it's relative to something else, you can adjust my example accordingly).
Since you seem to only be moving the image horizontally, the distance should be a simple first degree calculation.
Something like:
let distanceToCenter = abs(gesture.view!.center.x - gesture.view!.parent!.center.x)
If you were moving both vertically and horizontally, you would need a second degree calculation to get the distance:
let deltaX = gesture.view!.center.x - gesture.view!.parent!.center.x
let deltaY = gesture.view!.center.y - gesture.view!.parent!.center.y
let distanceToCenter = sqrt(deltaX*deltaX + deltaY*deltaY)
You could then compute the alpha for fading out as follows:
let alpha = 1 - 2 * distanceToCenter / gesture.view!.parent!.bounds.size.width
Note that Swift may need some type casting for these calculations (at least until it goes to math school and figures out number theory)

Set limit to draggable item in Swift

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.

can't access object within UIPanGestureRecognizer

I am doing a bezier animation with programmatically generated objects. The number of objects could be bigger than 100. These objects are also responsive to touch.
The animation works fine. Now I would like to make some objects drag-able.
However, I can't access the individual objects in the UIPanGestureRecognizer function. I assume I m doing something wrong with class / subclass calling, but can't think of it..
The tutorials I looked into had IBOutlets or dedicated class variables for every animated object on screen.. I do have a potential high number of objects that are generated within the FOR loop..
What approach do you suggest?
func panning(pan: UIPanGestureRecognizer) {
self.view.bringSubviewToFront(pan.view!)
var translation = pan.translationInView(self.view)
pan.view.center = CGPointMake(pan.view.center.x + translation.x, pan.view.center.y + translation.y)
pan.setTranslation(CGPointZero, inView: self.view)
let panGesture = UIPanGestureRecognizer(target: self, action: "panning:");
}
}
.. within viewDidLoad the function gets called:
// loop from 0 to 50
for i in 0...50 {
// create a square object
let square = UIView()
square.frame = CGRect(x: 55, y: 300, width: 40, height: 40)
square.backgroundColor = UIColor.redColor()
self.view.addSubview(square)
let recognizer = UIPanGestureRecognizer(target: self, action: "panning:");
recognizer.delegate = self;
square.addGestureRecognizer(recognizer)
panGesture.delegate = self;
Thanks to the feedback from Rob, here's an updated version:
I don't really know how to do add the AllowUserInteraction to CAKeyframeAnimation (Rob's answer 3a), as it does not have an "option" field (I m a noob).
Stopping the animation for the object being moved (Rob's answer 3b) completely eludes me. But its something definitely necessary in here.
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
srand48(Int(NSDate().timeIntervalSince1970))
// loop from 0 to 5
for i in 0...5 {
// create a square
let square = UIView()
square.frame = CGRect(x: 55, y: 300, width: 40, height: 40)
square.backgroundColor = UIColor.redColor()
self.view.addSubview(square)
//square.userInteractionEnabled = true;
let recognizer = UIPanGestureRecognizer(target: self, action: "panning:");
square.addGestureRecognizer(recognizer)
// randomly create a value between 0.0 and 150.0
let randomYOffset = CGFloat( drand48() * 150)
// for every y-value on the bezier curve
// add our random y offset so that each individual animation
// will appear at a different y-position
let path = UIBezierPath()
path.moveToPoint(CGPoint(x: -16,y: 239 + randomYOffset))
path.addCurveToPoint(CGPoint(x: 375, y: 239 + randomYOffset), controlPoint1: CGPoint(x: 136, y: 373 + randomYOffset), controlPoint2: CGPoint(x: 178, y: 110 + randomYOffset))
// create the animation
let anim = CAKeyframeAnimation(keyPath: "position")
anim.path = path.CGPath
anim.rotationMode = kCAAnimationRotateAuto
anim.repeatCount = Float.infinity
//anim.duration = 5.0
// each square will take between 4.0 and 8.0 seconds
// to complete one animation loop
anim.duration = 4.0 + 3 * drand48()
// stagger each animation by a random value
// `290` was chosen simply by experimentation
anim.timeOffset = 290 * drand48()
// add the animation
square.layer.addAnimation(anim, forKey: "animate position along path")
}
}
func panning(pan: UIPanGestureRecognizer) {
if pan.state == .Began {
println("pan began")
self.view.bringSubviewToFront(pan.view!)
} else if pan.state == .Changed {
println("pan state changed")
var translation = pan.translationInView(self.view)
pan.view?.center = CGPointMake(pan.view!.center.x + translation.x, pan.view!.center.y + translation.y)
pan.setTranslation(CGPointZero, inView: self.view)
println("translation")
}
}
Two things jump out at me:
Your for loop is setting delegates for recognizer (which is not needed unless you're doing something particular which you haven't shared with us) and panGesture (which should be removed because panGesture is not a variable that you have set in this for loop and seems to be completely unrelated). Thus it would be:
for i in 0...50 {
let square = UIView()
square.frame = CGRect(x: 55, y: 300, width: 40, height: 40)
square.backgroundColor = UIColor.redColor()
self.view.addSubview(square)
let recognizer = UIPanGestureRecognizer(target: self, action: "panning:");
square.addGestureRecognizer(recognizer)
}
In panning you are instantiating a new panGesture: You definitely want to get rid of that. A gesture handler has no business creating new gestures. So get rid of the line that starts with let panGesture = ....
In your original question, you hadn't shared the animation you're doing. If you were animating using block-based animation, you have to specify the UIViewAnimationOptions.AllowUserInteraction options. Also, if you'd start dragging it around a view that is being animated, you'd also want to (a) stop the animation for that view; and (b) reset the frame in accordance with the presentationLayer.
But, it's now clear that you're using CAKeyframeAnimation, in which case, there is no AllowUserInteraction mechanism. So, instead, you have to add the gesture recognizer to the superview, and then iterate through the frames of the squares (as represented by their presentation layer, i.e. the location mid-animation) and test to see if you get any hits.
It's up to you, but I find the pan gesture is a little slow to start recognizing the gesture (as it differentiates between a pan and a tap. In this case, you want something a little more responsive, methinks, so I might use a long press gesture recognizer with a
So, pulling that all together, you end up with something like:
var squares = [UIView]() // an array to keep track of all of the squares
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
srand48(Int(NSDate().timeIntervalSince1970))
// loop from 0 to 5
for i in 0...5 {
// create a square
let square = UIView()
square.frame = CGRect(x: 55, y: 300, width: 40, height: 40)
square.backgroundColor = UIColor.redColor()
self.view.addSubview(square)
squares.append(square)
// randomly create a value between 0.0 and 150.0
let randomYOffset = CGFloat( drand48() * 150)
// for every y-value on the bezier curve
// add our random y offset so that each individual animation
// will appear at a different y-position
let path = UIBezierPath()
path.moveToPoint(CGPoint(x: -16,y: 239 + randomYOffset))
path.addCurveToPoint(CGPoint(x: 375, y: 239 + randomYOffset), controlPoint1: CGPoint(x: 136, y: 373 + randomYOffset), controlPoint2: CGPoint(x: 178, y: 110 + randomYOffset))
// create the animation
let anim = CAKeyframeAnimation(keyPath: "position")
anim.path = path.CGPath
anim.rotationMode = kCAAnimationRotateAuto
anim.repeatCount = Float.infinity
//anim.duration = 5.0
// each square will take between 4.0 and 8.0 seconds
// to complete one animation loop
anim.duration = 4.0 + 3 * drand48()
// stagger each animation by a random value
// `290` was chosen simply by experimentation
anim.timeOffset = 290 * drand48()
// add the animation
square.layer.addAnimation(anim, forKey: "animate position along path")
}
let recognizer = UILongPressGestureRecognizer(target: self, action: "handleLongPress:");
recognizer.minimumPressDuration = 0
view.addGestureRecognizer(recognizer)
}
var viewToDrag: UIView! // which of the squares are we dragging
var viewToDragCenter: CGPoint! // what was the `center` of `viewToDrag` when we started to drag it
var originalLocation: CGPoint! // what was gesture.locationInView(gesture.view!) when we started dragging
func handleLongPress(gesture: UILongPressGestureRecognizer) {
let location = gesture.locationInView(gesture.view!)
if gesture.state == .Began {
for square in squares {
let presentationLayer = square.layer.presentationLayer() as CALayer
let frame = presentationLayer.frame
if CGRectContainsPoint(frame, location) {
viewToDrag = square
viewToDragCenter = CGPointMake(CGRectGetMidX(frame), CGRectGetMidY(frame))
viewToDrag.layer.removeAnimationForKey("animate position along path")
viewToDrag.center = viewToDragCenter
originalLocation = location
return
}
}
} else if gesture.state == .Changed {
if viewToDrag != nil {
var translation = CGPointMake(location.x - originalLocation.x, location.y - originalLocation.y)
viewToDrag.center = CGPointMake(viewToDragCenter.x + translation.x, viewToDragCenter.y + translation.y)
}
} else if gesture.state == .Ended {
viewToDrag = nil
}
}

Resources