Animating button border in swift - swift

I recently learned how to animate a button using alpha so that it fades in/out in Swift. However, if I would like to only have the alpha of the border itself change, the animation does not seem to be working. Instead, it "jumps" from state to state.
UIView.animateWithDuration(1.0, delay: 0.0, options: nil, animations: {
var borderColor = UIColor(red: 0.41, green: 1.28, blue: 1.85, alpha: 0.0)
self.startButton.layer.borderColor = borderColor.CGColor
}, completion: nil);
The above code for example does not animate, instead it produces a "jump" between alpha 1.0 and 0.0 of the border.
However, this would work fine (changing the alpha of the entire button):
UIView.animateWithDuration(1.0, delay: 0.0, options: nil, animations: {
self.startButton.alpha = 1;
}, completion: nil);
Is there any way to get around this issue?

Here's a simple solution:
let borderWidth:CABasicAnimation = CABasicAnimation(keyPath: "borderWidth")
borderWidth.fromValue = 0
borderWidth.toValue = 0.9
borderWidth.duration = 0.5
yourView.layer.borderWidth = 0.5
yourView.layer.borderColor = UIColor.whiteColor().CGColor
yourView.layer.addAnimation(borderWidth, forKey: "Width")
yourView.layer.borderWidth = 0.0
It works for me.

You can't animate parts of the layer using UIView animations. Also, I don't think it's possible to animate the alpha value of the border. To achieve a similar effect you can animate the width of your border:
I made this UIView extension:
extension UIView {
func animateBorderWidth(toValue: CGFloat, duration: Double = 0.3) {
let animation = CABasicAnimation(keyPath: "borderWidth")
animation.fromValue = layer.borderWidth
animation.toValue = toValue
animation.duration = duration
layer.add(animation, forKey: "Width")
layer.borderWidth = toValue
}
}
and then show the border of your view by using:
startButton.animateBorderWidth(toValue: 1, duration: 0.5)
and hide it by using:
startButton.animateBorderWidth(toValue: 0)

Features such as the borderColor and borderWidth are properties of the button's layer. That tells you that you must use core animation of the layer - not view animation, as your code tries to do - to animate such features. It works perfectly well; in this animation, I demonstrate what happens when you use core animation to animation the borderWidth and the cornerRadius together:
Animation of the borderColor would work similarly.

Related

Why is my Swift button animation growing back to full size?

What I expect: That when I press down on the button it shrinks to 75% and stays at its 75% shrunken size until I let go.
What is happening: The buttons shrinks to 75%, but as soon as it finishes the duration of the animation, while my finger is still pressed on the button, it grows back to its original value.
I am called the following when a button is "Touched Down".
import Foundation
import UIKit
extension UIButton {
func shrink() {
let shrink = CABasicAnimation(keyPath: "transform.scale")
shrink.fromValue = 1.0
shrink.toValue = 0.75
shrink.duration = 0.5
shrink.isRemovedOnCompletion = false
layer.add(shrink, forKey: nil)
}
}
The size resets because CABasicAnimation doesn't update the layer's underlying value, only the presentation layer. So when the animation ends, it goes back to using the underlying value.
The best way to deal with this is to set the transform scale before the animation starts so that it is the correct value at the end.
func shrink() {
transform = CGAffineTransform(scaleX: 0.75, y: 0.75) // Set final state
let shrink = CABasicAnimation(keyPath: "transform.scale")
shrink.fromValue = 1.0
shrink.toValue = 0.75
shrink.duration = 0.5
layer.add(shrink, forKey: nil)
}
You can also set the fillMode to .forwards and isRemovedOnCompletion to false, which will keep the presentation layer changes in place, but this doesn't update the layer's actual scale, so the button will keep the original scale for tap detection. (You need to set the transform's scale as above to correct it.)
func shrink() {
let shrink = CABasicAnimation(keyPath: "transform.scale")
shrink.fromValue = 1.0
shrink.toValue = 0.75
shrink.duration = 0.5
shrink.isRemovedOnCompletion = false
shrink.fillMode = .forwards
layer.add(shrink, forKey: nil)
}
You can also use a view animation which will have the same visual effect, but also update the scale:
UIView.animate(withDuration: 0.5) { [weak self] in
self?.transform = CGAffineTransform(scaleX: 0.75, y: 0.75)
}

Center a view inside of an UITableViewCell? Looks centered in the UI hierarchy, but not in the actual app?

I'm trying to center a custom UIView in a UITableViewCell. Something fairly basic imo. I used this code to create the UIView, I found it here on stackoverflow:
class SkeletonView: UIView {
var startLocations : [NSNumber] = [-1.0,-0.5, 0.0]
var endLocations : [NSNumber] = [1.0,1.5, 2.0]
var gradientBackgroundColor : CGColor = UIColor(white: 0.85, alpha: 1.0).cgColor
var gradientMovingColor : CGColor = UIColor(white: 0.75, alpha: 1.0).cgColor
var movingAnimationDuration : CFTimeInterval = 0.8
var delayBetweenAnimationLoops : CFTimeInterval = 1.0
lazy var gradientLayer : CAGradientLayer = {
let gradientLayer = CAGradientLayer()
gradientLayer.frame = self.bounds
gradientLayer.startPoint = CGPoint(x: 0.0, y: 1.0)
gradientLayer.endPoint = CGPoint(x: 1.0, y: 1.0)
gradientLayer.colors = [
gradientBackgroundColor,
gradientMovingColor,
gradientBackgroundColor
]
gradientLayer.locations = self.startLocations
self.layer.addSublayer(gradientLayer)
return gradientLayer
}()
func startAnimating(){
let animation = CABasicAnimation(keyPath: "locations")
animation.fromValue = self.startLocations
animation.toValue = self.endLocations
animation.duration = self.movingAnimationDuration
animation.timingFunction = CAMediaTimingFunction(name: CAMediaTimingFunctionName.easeInEaseOut)
let animationGroup = CAAnimationGroup()
animationGroup.duration = self.movingAnimationDuration + self.delayBetweenAnimationLoops
animationGroup.animations = [animation]
animationGroup.repeatCount = .infinity
self.gradientLayer.add(animationGroup, forKey: animation.keyPath)
}
func stopAnimating() {
self.gradientLayer.removeAllAnimations()
}
}
Then I just added a view to my cell xib, centered horizontally, leading edge +15, top +15 and bottom +15 (with priority 900 so that autolayout doesn't freak out about the height being 320 instead of its calculated value of 320.1). This way it is centered in the xib preview.
When running the app it looks like this, as you can see there is a gap on the left side, but not on the right side, so its not centered:
But then when I take a look at the UI hierarchy, it is centered?!
Remove centered horizontally contraint and add trailing constraint to 15 will resolve your issue
its good enough on my side
Nevermind, I found the problem. The UIView was placed correctly, however the gradientLayer I placed overtop was getting a wrong size. So I added the line
gradientLayer.frame = self.bounds
in the layoutSubviews function inside the skeleton UIView class.

Animating the text color of a UIView extension

I have an extension of UILabel with a shake method. This method shakes the label for 2 seconds. It works great.
I am trying to chain a 2 second red -> white text-color change to the label text. It does not work great.
Here's my progress.
extension UILabel {
func shake() {
let animation = CAKeyframeAnimation(keyPath: "transform.translation.x")
animation.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionLinear)
animation.duration = 2
animation.values = [
0.0, 0.0,
-4.0, 4.0,
-7.0, 7.0,
-10.0, 10.0,
-7.0, 7.0,
-4.0, 4.0,
0.0, 0.0,
]
animation.repeatCount = 1
layer.add(animation, forKey: nil)
let colorsAnimation = CABasicAnimation(keyPath: "foregroundColor")
colorsAnimation.fromValue = UIColor.red.cgColor
colorsAnimation.toValue = UIColor.white.cgColor
colorsAnimation.duration = 2
layer.add(colorsAnimation, forKey: nil)
}
}
The problem:
The colorsAnimation CABasicAnimation has no effect on the text color. The text color does not make a 2 second transition from red to white.
How can I chain the shake animation and colorsAnimation together on the label?
CALayers don't have a foregroundColor property. They do have a backgroundColor property. You could animate that. (Simply change the line let colorsAnimation = CABasicAnimation(keyPath: "foregroundColor") to let colorsAnimation = CABasicAnimation(keyPath: "backgroundColor")
Why are you using CAAnimation rather than UIView animation? CAAnimation can do things that UIView animation cannot, but it is a lot harder to use and not as well documented.
(There is a keyframe UIView animation, for example.)

Animation in viewDidAppear not working after going background in Swift 3

I'm currently developing a countdown app in Swift 3. Behind the counter, I created a circle, which is animated according to the counter.
My problem is simple: when I click home and I put the app in the background, the animation doesn't work any more when I come back to the app. I don't know why. Interesting fact: the "drawing" is a plain circle, and only the border is animated. The circle's background is still ok, it is only the border animation which is not working any more.
Here is my code :
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
let screen = self.view.frame.size.width
let y = CGFloat(190)
let circleWidth = CGFloat(200)
let circleHeight = circleWidth
// Create a new CircleView
let circleView = CircleView(frame: CGRect(x: (screen/2) - (circleWidth/2), y: y, width: circleWidth, height: circleHeight))
view.addSubview(circleView)
// Animate the drawing of the circle over the course of 1 second
circleView.animateCircle(TimeInterval(seconds))
}
CircleView class:
import Foundation
import UIKit
class CircleView: UIView{
var circleLayer: CAShapeLayer!
override init(frame: CGRect) {
super.init(frame: frame)
self.backgroundColor = UIColor.clear
// Use UIBezierPath as an easy way to create the CGPath for the layer.
// The path should be the entire circle.
let circlePath = UIBezierPath(arcCenter: CGPoint(x: frame.size.width / 2.0, y: frame.size.height / 2.0), radius: (frame.size.width - 10)/2, startAngle: CGFloat(-M_PI/2), endAngle: CGFloat(M_PI*1.5), clockwise: true)
// Setup the CAShapeLayer with the path, colors, and line width
circleLayer = CAShapeLayer()
circleLayer.path = circlePath.cgPath
circleLayer.fillColor = UIColor(red: 255/255, green: 255/255, blue: 255/255, alpha: 0.2).cgColor
circleLayer.strokeColor = UIColor(red: 165/255, green: 219/255, blue: 255/255, alpha: 0.8).cgColor
circleLayer.lineWidth = 2.0;
// Don't draw the circle initially
circleLayer.strokeEnd = 0.0
// Add the circleLayer to the view's layer's sublayers
layer.addSublayer(circleLayer)
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func animateCircle(_ duration: TimeInterval) {
// We want to animate the strokeEnd property of the circleLayer
let animation = CABasicAnimation(keyPath: "strokeEnd")
// Set the animation duration appropriately
animation.duration = duration
// Animate from 0 (no circle) to 1 (full circle)
animation.fromValue = 1
animation.toValue = 0
// Do a linear animation (i.e. the speed of the animation stays the same)
animation.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionLinear)
// Set the circleLayer's strokeEnd property to 1.0 now so that it's the
// right value when the animation ends.
circleLayer.strokeEnd = 0.0
// Do the actual animation
circleLayer.add(animation, forKey: "animateCircle")
}
}
All you need to do is add one more line in your animation code which is:
animation.isRemovedOnCompletion = false
and your code will be:
func animateCircle(_ duration: TimeInterval) {
// We want to animate the strokeEnd property of the circleLayer
let animation = CABasicAnimation(keyPath: "strokeEnd")
// Set the animation duration appropriately
animation.duration = duration
// Animate from 0 (no circle) to 1 (full circle)
animation.fromValue = 1
animation.toValue = 0
// Do a linear animation (i.e. the speed of the animation stays the same)
animation.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionLinear)
animation.isRemovedOnCompletion = false
// Set the circleLayer's strokeEnd property to 1.0 now so that it's the
// right value when the animation ends.
circleLayer.strokeEnd = 0.0
// Do the actual animation
circleLayer.add(animation, forKey: "animateCircle")
}

Swift : Color Changing SKSpriteNode on repeat

I want to change the color of my SKSpriteNode similar to a strobe light how it constant changes colors then repeats. I can do it with a UIView like this:
UIView.animateWithDuration(2, delay: 0.0, options:[UIViewAnimationOptions.Repeat, UIViewAnimationOptions.Autoreverse], animations: {
self.view.backgroundColor = UIColor.blackColor()
self.view.backgroundColor = UIColor.greenColor()
self.view.backgroundColor = UIColor.grayColor()
self.view.backgroundColor = UIColor.redColor()
}, completion: nil)
but i want to do it to a sksprite node. I did something like this and created a bunch of them but it was problematic, and i feel there might be a better way.
let changeBlue = SKAction.colorizeWithColor(UIColor.blueColor(), colorBlendFactor: 1.0, duration: 2.0)
self.runAction(changeBlue)
let changeRed = SKAction.colorizeWithColor(UIColor.redColor(), colorBlendFactor: 1.0, duration: 2.0)
self.runAction(changeRed)
You're probably looking for sequence(_:). See Creating Actions That Combine or Repeat Other Actions in the SKAction docs. Also, repeatActionForever(_:) to make the action repeat indefinitely.
let colorBlue = SKAction.colorizeWithColor(UIColor.blueColor(), colorBlendFactor: 1.0, duration: 2.0)
let colorRed = SKAction.colorizeWithColor(UIColor.redColor(), colorBlendFactor: 1.0, duration: 2.0)
let colorizeSequence = SKAction.sequence([colorBlue, colorRed])
runAction(SKAction.repeatActionForever(colorizeSequence))