Search code examples
iosswiftanimationtouchesbegan

Animated UIView object jumps when touchesBegan is invoked


I have a UILabel object which is being animated, moving up and down. I am coding in Xcode 8.3 with Swift 3. A user can pan this object and drag around to one location to get some points. I am handling the pan/drag using touchesXXX gesture. When I tap and let it go immediately, however, this object jumps vertically above its location for some reason and I am unable to figure this out why for a week now...

When I enabled some debugging, I can only see touchesBegan was invoked (touchesMoved was not called as expected, neither touchesEnded which appears unexpected to me). If I disable animation on the object manually, it works as expected and the object is able to be panned effortlessly and there is obviously no object jumps.

Here is an extract of the relevant code:

override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
    let touch = touches.first
    let touchLocation = touch!.location(in: self.view)

    if self.optOneLbl.layer.presentation()!.hitTest(touchLocation) != nil {
        //pauseLayer(self.optOneLbl.layer)      <<<<< comment #1
        //optOneLbl.translatesAutoresizingMaskIntoConstraints = true      <<<<< comment #2
        optOneLbl.center = touchLocation
    }
}

override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
    let touch = touches.first
    let touchLocation = touch!.location(in: self.view)
    var sender: UILabel

    if self.optOneLbl.layer.presentation()!.hitTest(touchLocation) != nil {
        self.optOneLbl.center = touchLocation
        sender = self.optOneLbl
    }

    // identify which letter was overlapped
    var overlappedView : UILabel
    if (sender.frame.contains(letter1.center)) {
        ...
    }
    else {
        return
    }
}

override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
    super.touchesEnded(touches, with: event)
}

After reading responses to other SO questions, I thought disabling the label animation programmatically when touched as per above comment #1 in touchesBegan might help, but the issue persisted. Also, I thought may be Auto Layout is causing this weird jump. So, I enabled translatesAutoresizingMaskIntoConstraints as per comment #2, but it too didn't help.

Anyone is able to see where I am handling this incorrectly? Thank you for reading!

EDIT:

As per @agibson007 request, I am adding the animation code extract for reference:

UIView.animate(withDuration: 12.0, delay: 0.0, options: [ .allowUserInteraction, .curveLinear, .autoreverse, .repeat ], animations: {
    self.optOneLbl.center = CGPoint(x: self.optOneLbl.center.x, y: screenSize.midY*1.1)
})

Solution

  • You need to remove/reset the animation after you change the location of the label. The animation does not know you updated the values and is trying to stay in the same range of byValue from the beginning. Here is a working example of updating the animation.

    import UIKit
    
    class ViewController: UIViewController {
    
        var optOneLbl = UILabel()
    
    
        override func viewDidLoad() {
            super.viewDidLoad()
            // Do any additional setup after loading the view, typically from a nib.
    
    
            optOneLbl = UILabel(frame: CGRect(x: 20, y: 50, width: self.view.bounds.width - 40, height: 40))
            optOneLbl.textAlignment = .center
            optOneLbl.text = "I Will Be Moving Up and Down"
            optOneLbl.textColor = .white
            optOneLbl.backgroundColor = .blue
            self.view.addSubview(optOneLbl)
    
            //starts it in the beginnning with a y
            fireAnimation(toY:  self.view.bounds.midY*1.1)
    
    
        }
    
        func fireAnimation(toY:CGFloat) {
            UIView.animate(withDuration: 12.0, delay: 0.0, options: [ .allowUserInteraction, .curveLinear, .autoreverse, .repeat ], animations: {
                self.optOneLbl.center = CGPoint(x: self.optOneLbl.center.x, y:toY)
            })
        }
    
    
    
        override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
            let touch = touches.first
            let touchLocation = touch!.location(in: self.view)
    
            if self.optOneLbl.layer.presentation()!.hitTest(touchLocation) != nil {
                //re
                optOneLbl.layer.removeAllAnimations()
                optOneLbl.center = touchLocation
            }
        }
    
        override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
            let touch = touches.first
            let touchLocation = touch!.location(in: self.view)
            var sender: UILabel
    
            if self.optOneLbl.layer.presentation()!.hitTest(touchLocation) != nil {
                self.optOneLbl.center = touchLocation
                sender = self.optOneLbl
            }
        }
    
        override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
            super.touchesEnded(touches, with: event)
            //restart animation after finished and change the Y if you want.  
            // you could change duration or whatever
            fireAnimation(toY: self.view.bounds.height - optOneLbl.bounds.height)
        }
    
    }