UIButton that has an interupptable animation with UIViewPropertyAnimator - ios

I want a button to expand (Scale up) when the user touches the button, and shrink back to normal when the touch leaves. Similar to iOS stock calculator before iOS 11. This is the class that I created a subclass of the button. It uses touchesbegan and touchesended to start, and or pause and revers the animation, but it does not shrink properly, and it doesn't animate.
class UIOutlineButton: UIButton {
var squishAnimation:UIPropertyAnimator!
override public func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
super.touchesBegan(touches, with: event)
squishAnimation = UIViewPropertyAnimator(duration: 0.5, dampingRatio: 30, animations: {
let scale = self.transform.scaledBy(x: 0.8, y: 0.8)
let rotate = self.transform.rotated(by: 0.3)
self.transform.concatenating(rotate)
self.transform = scale
})
squishAnimation.isReversed = true
squishAnimation.startAnimation()
self.next?.touchesBegan(touches, with: event)
}
override public func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
super.touchesEnded(touches, with: event)
colorAnimation = UIViewPropertyAnimator(duration: 0.5, dampingRatio: 30, animations: {
})
squishAnimation = UIViewPropertyAnimator(duration: 0.5, dampingRatio: 30, animations: {
self.transform = CGAffineTransform.identity
})
squishAnimation.stopAnimation(true)
squishAnimation.startAnimation()
}
}

Here is an little snippet which will 'grow' a UIView and then return it to it's original size after a short delay:
/// Expands & Then Returns A UIView To It's Original Size
///
/// - Parameters:
/// - view: UIView
/// - duration: Double
/// - enable: (Void)
func animate(view: UIView, duration: Double, enable: #escaping () -> ()){
view.isUserInteractionEnabled = false
UIView.animate(withDuration: duration, animations: {
view.transform = CGAffineTransform(scaleX: 0.8, y: 0.8)
}) { (continueSequence) in
UIView.animate(withDuration: duration, animations: {
view.transform = .identity
}, completion: { (seqeunceFinished) in
enable()
})
}
}
You can then call it within an IBAction as follows:
#IBAction func enlargeButton(_ sender: UIButton){
sender.isUserInteractionEnabled = false
enlarge(view: sender duration: 1) { sender.isUserInteractionEnabled = true }
}

Related

How to make table view to display like flip animation in all devices in swift 3?

In all devices the table view needs to be display only 70% of the screen irrespective of all devices and it should need to display only from bottom to top with animation and if I select anywhere on screen it should go to below and hide with animation I tried but unable to implement it can any one help me how to implement this ?
Here is my code
#IBAction func addtoCartButtonAction(_ sender: Any) {
UIView.animate(withDuration: 0.5, delay: 0.0, options: UIViewAnimationOptions.curveEaseIn, animations: {
self.addToCartTableView.isHidden = false
self.addToCartTableView.delegate = self
self.addToCartTableView.dataSource = self
self.addToCartTableView.reloadData()
self.addToCartTableView.frame.origin.y = 200
}) { (Bool) in
}
}
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
UIView.animate(withDuration: 1, delay: 0.3, options: .autoreverse, animations: {
}) { (Bool) in
self.addToCartTableView.isHidden = true
}
}
#IBAction func AddtoCartTableViewCloseButtonAction(_ sender: Any) {
self.addToCartTableView.isHidden = true
}
For this you can use CGAffineTransform.
For example :
in viewWillAppear() do this :
addToCartTableView.alpha = 0.0
addToCartTableView.transform = CGAffineTransform(translationX: addToCartTableView.bounds.origin.x, y: self.view.bounds.height)
And in you button action addtoCartButtonAction do this :
UIView.animate(withDuration: 0.5, delay: 0.0, options: UIViewAnimationOptions.curveEaseIn, animations: {
self.addToCartTableView.delegate = self
self.addToCartTableView.dataSource = self
self.addToCartTableView.reloadData()
self.addToCartTableView.transform = .identity
self.addToCartTableView.alpha = 1.0
}) { (Bool) in
}
And a tap gesture to your view and whenever you want to hide it again do this :
UIView.animate(withDuration: 0.5, delay: 0.0, options: UIViewAnimationOptions.curveEaseIn, animations: {
self.addToCartTableView.transform = CGAffineTransform(translationX: addToCartTableView.bounds.origin.x, y: self.view.bounds.height)
self.addToCartTableView.alpha = 0.0
}) { (Bool) in
}
Also you should not assign self.addToCartTableView.delegate = self self.addToCartTableView.dataSource = self in button action. Do that in viewDidLoad() or in storyboard only.

How to disable standard UIButton touch event

In my application I added a UIButton, but instead of having the standard touch event of the UIButton getting darker then becoming the standard color when touch events have ended, I want to add an animation that when you click on the UIButton the button gets smaller, then when touch events have ended the UIButton becomes back to regular size when touch events have ended. So far I have typed this code, but it does not seem to work. Please let me know how I can accomplish this animation.
// whatsTheGame is the UIButton I have
#IBAction func whatsTheGame(_ sender: UIButton) {
logo.isHighlighted = false
UIView.animate(withDuration: 0.3) {
if let centerLogo = self.logo {
centerLogo.transform = CGAffineTransform(scaleX: 0.7, y: 0.7)
}
}
}
override func touchesCancelled(_ touches: Set<UITouch>, with event: UIEvent?) {
super.touchesCancelled(touches, with: event)
UIView.animate(withDuration: 0.3) {
if let centerLogo = self.logo {
centerLogo.transform = CGAffineTransform(scaleX: 1, y: 1)
}
}
}
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
super.touchesEnded(touches, with: event)
UIView.animate(withDuration: 0.3) {
if let centerLogo = self.logo {
centerLogo.transform = CGAffineTransform(scaleX: 1, y: 1)
}
}
}
Adding Touch Up Inside and Touch Down events
#IBAction func onTouchUpInside(_ sender: Any) {
let button = sender as! UIButton
UIView.animate(withDuration: 0.3) {
button.transform = CGAffineTransform(scaleX: 1, y: 1)
}
}
#IBAction func onTouchDown(_ sender: Any) {
let button = sender as! UIButton
UIView.animate(withDuration: 0.3) {
button.transform = CGAffineTransform(scaleX: 0.7, y: 0.7)
}
}

Retrieving the right position of a scaled UIView

I'm having trouble retrieving the current position of a view, absolutely in the iPhone screen, when the view is scaled.
Here is a minimal example of what I've done.
I have a single elliptical view on screen. When I tap, it grows and start to shiver to show that it is selected. Then I can drag the view all over the screen.
Here is the code:
ViewController
import UIKit
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
// The View
let toDrag = MyView(frame: CGRect(x: 0, y: 0, width: 50, height: 40))
toDrag.center = CGPoint(x:100, y:100)
// The gesture recognizer
let panGestRec = UIPanGestureRecognizer(target: self, action:#selector(self.panView(sender:)))
toDrag.addGestureRecognizer(panGestRec)
self.view.addSubview(toDrag)
}
func panView(sender: UIPanGestureRecognizer) {
guard let senderView = sender.view else { return }
if sender.state == .recognized { print("Pan Recognized", terminator:"")}
if sender.state == .began { print("Pan began", terminator: "") }
if sender.state == .changed { print("Pan changed", terminator: "") }
if sender.state == .ended { print("Pan ended", terminator: "") }
let point = senderView.convert(senderView.center, to: nil)
print (" point \(point)")
let translation = sender.translation(in: self.view)
senderView.center = CGPoint(x: senderView.center.x + translation.x, y: senderView.center.y + translation.y)
sender.setTranslation(CGPoint.zero, in: self.view)
}
}
MyView
import UIKit
class MyView: UIView {
var isAnimated: Bool = false
override init(frame: CGRect) {
super.init(frame: frame) // calls designated initializer
self.isOpaque = false
self.backgroundColor = UIColor.clear
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
self.isOpaque = false
self.backgroundColor = UIColor.clear
}
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
isAnimated = true
rotate1()
}
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
isAnimated = false
endRotation()
}
override func touchesCancelled(_ touches: Set<UITouch>, with event: UIEvent?) {
isAnimated = false
endRotation()
}
override func draw(_ rect: CGRect) {
guard let context = UIGraphicsGetCurrentContext() else {return}
context.addEllipse(in: rect)
context.setLineWidth(1.0)
context.setStrokeColor(UIColor.black.cgColor)
context.setFillColor(UIColor.red.cgColor)
context.drawPath(using: .fillStroke)
}
func rotate1() {
guard isAnimated else { return }
UIView.animate(withDuration: 0.05, delay:0.0, options: [], animations: {
self.transform = CGAffineTransform.init(rotationAngle:.pi/16).scaledBy(x: 1.5, y: 1.5)
}, completion: { _ in
self.rotate2()
})
}
func rotate2() {
guard isAnimated else { return }
UIView.animate(withDuration: 0.05, delay:0.0, options: [], animations: {
self.transform = CGAffineTransform.init(rotationAngle:.pi/(-16)).scaledBy(x: 1.5, y: 1.5)
}, completion: { _ in
self.rotate1()
})
}
func endRotation () {
// End existing animation
UIView.animate(withDuration: 0.5, delay:0.0, options: .beginFromCurrentState, animations: {
self.transform = CGAffineTransform.init(rotationAngle:0 ).scaledBy(x: 1, y: 1)
}, completion: nil)
}
}
With this version, here are some displays of the position
Pan began point (233.749182687299, 195.746572421573)
Pan changed point (175.265022520054, 181.332358181369)
Pan changed point (177.265022520054, 184.332358181369)
Pan changed point (177.265022520054, 184.332358181369)
Pan changed point (178.265022520054, 185.332358181369)
Pan changed point (179.265022520054, 186.332358181369)
Pan changed point (180.265022520054, 187.332358181369)
Pan changed point (180.265022520054, 188.332358181369)
Pan changed point (181.265022520054, 188.332358181369)
Pan RecognizedPan ended point (181.265022520054, 189.332358181369)
You can see that the first position (began point) isn't correct: the view was animated at that moment. The other positions are OK, when dragging, the view don't animate.
If I comment the code in rotate1 and rotate2, all the positions are correct, so I assume that the sizing and eventually the rotation of the view are interfering in the result. My question is: how can I retrieve the correct position of the view, when it is scaled? Obviously, the line
senderView.convert(senderView.center, to: nil)
didn't make what I thought : converting the coordinate to the fixed size screen coordinates?
I tried changing the animation from View animation to Core Animation (layer), and the result was the same.
So I tried to imagine a formula, with size, bounds and frame values, that could result in the correct position, but I did not succeed...
The only think that worked, was to stop immediately the animation with a transform, just before trying to retrieve the center, like that in panView
func panView(sender: UIPanGestureRecognizer) {
guard let senderView = sender.view else { return }
// Stop the transformation now!
senderView.transform = CGAffineTransform.init(rotationAngle:0 ).scaledBy(x: 1, y: 1)
if sender.state == .recognized { print("Pan Recognized", terminator:"")}
...
It isn't the solution I wanted, but I'll go with this for now...
In fact, I had another problem with view convert (Retrieve the right position of a subView with auto-layout)
and my problem was the same, so the solution is here...
I didn't use the method correctly. If I change using beninho85's solution (and even more cleaner cosyn's method to use just one view), it works! I can have the right coordinates even if the view is animated.
I just had to replace, in ViewController :
let point = senderView.convert(senderView.center, to: nil)
with
let point = senderView.convert(CGPoint(x:senderView.bounds.midX, y:senderView.bounds.midY), to: nil)

Cancel UIControl animateWithDuration to start a new animation

I have troubles in creating a very simple square button with the following behaviour:
When I set my finger on the button it should shrink down a bit (animated)
When I hold my finger on the button the button should keep at its small state.
When I lift my finger the button should grow back to its initial size (animated).
The problem is, when I touch/lift up while the animation is playing the button should stop the current animation and shrink/grow. Right now I can only interact with the button after the grow/shrink animation is fully played.
I tried to use the layer.removeAllAnimations() function in the touch functions ... without success. I tried to animate the View itself and the layer.
Here is my attempt of the UIControl that I used to build my custom button.
import UIKit
class TriggerPad: UIControl {
required init(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
override init(frame: CGRect) {
// I init the Frame with w:200 h:100 in my ViewController
super.init(frame: frame)
self.backgroundColor = UIColor.clearColor()
self.layer.frame = frame
self.layer.backgroundColor = UIColor.blueColor().CGColor
self.multipleTouchEnabled = true
}
override func touchesBegan(touches: NSSet, withEvent event: UIEvent) {
let oldFrame = self.layer.frame
self.layer.removeAllAnimations()
UIView.animateWithDuration(2) { () -> Void in
self.layer.frame.size.height = 50
self.layer.frame.size.width = 100
// I need to adjust the origin here
}
}
override func touchesEnded(touches: NSSet, withEvent event: UIEvent) {
let oldFrame = self.layer.frame
self.layer.removeAllAnimations()
UIView.animateWithDuration(2) { () -> Void in
self.layer.frame.size.height = 100
self.layer.frame.size.width = 200
// I need to adjust the origin here
}
}
}
You can solve it adding to the UIView the animation option .AllowUserInteraction. Your code would be:
override func touchesBegan(touches: NSSet, withEvent event: UIEvent) {
UIView.animateWithDuration(2.0, delay: 0.0, options: UIViewAnimationOptions.AllowUserInteraction, animations: { () -> Void in
self.layer.frame.size.height = 50
self.layer.frame.size.width = 100
// I need to adjust the origin here
}, completion: nil)
}
override func touchesEnded(touches: NSSet, withEvent event: UIEvent) {
UIView.animateWithDuration(2.0, delay: 0.0, options: UIViewAnimationOptions.AllowUserInteraction, animations: { () -> Void in
self.layer.frame.size.height = 100
self.layer.frame.size.width = 200
// I need to adjust the origin here
}, completion: nil)
}
But I think you can get the similar effect to the App you mention with:
override func touchesBegan(touches: NSSet, withEvent event: UIEvent) {
UIView.animateWithDuration(2.0, delay: 0.0, options: UIViewAnimationOptions.AllowUserInteraction, animations: { () -> Void in
self.transform = CGAffineTransformMakeScale(0.6, 0.6)
}, completion: nil)
}
override func touchesEnded(touches: NSSet, withEvent event: UIEvent) {
UIView.animateWithDuration(2.0, delay: 0.0, options: UIViewAnimationOptions.AllowUserInteraction, animations: { () -> Void in
self.transform = CGAffineTransformIdentity
}, completion: nil)
}
Because this way it keeps the view's center.

UIButton Subclass #IBAction not working

I created a sub class for a UIButton so I could get an animation effect on tap, the effect works, what doesn't work is when I try took hook it up to a #IBAction in my view controller. The function isn't getting called. I have the sub class set as the buttons class in my scoreboard. Here's my code:
import UIKit
class SpringyButton: UIButton {
/*
// Only override drawRect: if you perform custom drawing.
// An empty implementation adversely affects performance during animation.
override func drawRect(rect: CGRect) {
// Drawing code
}
*/
override func touchesBegan(touches: NSSet, withEvent event: UIEvent) {
UIView.animateWithDuration(0.4, delay: 0.0, usingSpringWithDamping: 0.9, initialSpringVelocity: 0.9, options: UIViewAnimationOptions.AllowUserInteraction, animations: {
self.layer.transform = CATransform3DMakeScale(0.8, 0.8, 1)
}, completion: nil)
}
override func touchesEnded(touches: NSSet, withEvent event: UIEvent) {
UIView.animateWithDuration(0.4, delay: 0.0, usingSpringWithDamping: 0.9, initialSpringVelocity: 0.9, options: UIViewAnimationOptions.AllowUserInteraction, animations: {
self.layer.transform = CATransform3DMakeScale(1, 1, 1)
}, completion: nil)
}
override func touchesCancelled(touches: NSSet!, withEvent event: UIEvent!) {
UIView.animateWithDuration(0.4, delay: 0.0, usingSpringWithDamping: 0.9, initialSpringVelocity: 0.9, options: UIViewAnimationOptions.AllowUserInteraction, animations: {
self.layer.transform = CATransform3DMakeScale(1, 1, 1)
}, completion: nil)
}
override func touchesMoved(touches: NSSet, withEvent event: UIEvent) {
UIView.animateWithDuration(0.4, delay: 0.0, usingSpringWithDamping: 0.9, initialSpringVelocity: 0.9, options: UIViewAnimationOptions.AllowUserInteraction, animations: {
self.layer.transform = CATransform3DMakeScale(1, 1, 1)
}, completion: nil)
}
}
and my IBAction in my ViewController:
#IBAction func test(sender: AnyObject) {
println("go")
}
I'm not sure what's causing this, any ideas?
Side Note: I've read that subclassing a UIButton isn't such a great idea, would it be better to subclass UIView and hook that up with a tap gesture recognizer?
Thanks.
The problem is that you just override the touch events. You have to call super after overriding the function. For example in the touchesBegan function below:
override func touchesBegan(touches: NSSet, withEvent event: UIEvent) {
UIView.animateWithDuration(0.4, delay: 0.0, usingSpringWithDamping: 0.9, initialSpringVelocity: 0.9, options: UIViewAnimationOptions.AllowUserInteraction, animations: {
self.layer.transform = CATransform3DMakeScale(0.8, 0.8, 1)
}, completion: nil)
super.touchesBegan(touches, withEvent: event)
}
like this you have to add super to all of your touch events that you override. If you encounter any problem please let me know.
I believe that Interface Builder only uses UIButton. So subclassing won't really work. If you really want to use your custom class, you can programmatically create it.
let myButton : SpringyButton = SpringyButton()
Let me know if that works and/or if that's what you were looking for!

Resources