CABasicAnimation with keypath "bounds" not working - swift

I have the following code to animation bounds property of CALayer using CABasicAnimation. But the code doesn't seem to work.
let fromValue = textLabel.layer.bounds
let toValue = CGRectMake(textLabel.layer.bounds.origin.x, textLabel.layer.bounds.origin.y, textLabel.layer.bounds.width, textLabel.layer.bounds.height + 50)
let positionAnimation = CABasicAnimation(keyPath: "bounds")
positionAnimation.fromValue = NSValue(CGRect: fromValue)
positionAnimation.toValue = NSValue(CGRect: toValue)
positionAnimation.duration = 1
positionAnimation.fillMode = kCAFillModeBoth
positionAnimation.removedOnCompletion = false
textLabel.layer.addAnimation(positionAnimation, forKey: "bounds")

Your code does actually work. If you run your code and then switch on View Debugging in Xcode, you will see that the height of the label has increased. The "problem" is that a UILabel in iOS 8 draws itself (its text and its background, if it has one) the same way even after its layer height has been artificially increased in this way. (I believe that this is because the label draws itself with a special clipping region that is based on its text contents.)
To prove this to yourself, try it on a plain vanilla UIView (with a colored background) instead of a label. I've taken the liberty of cleaning up your code (you should never misuse fillMode and removedOnCompletion the way you are doing - it just shows a lack of understand of what animation is):
let fromValue = view2.layer.bounds.height
let toValue = view2.layer.bounds.height + 50
CATransaction.setDisableActions(true)
view2.layer.bounds.size.height = toValue
let positionAnimation = CABasicAnimation(keyPath: "bounds.size.height")
positionAnimation.fromValue = fromValue
positionAnimation.toValue = toValue
positionAnimation.duration = 1
view2.layer.addAnimation(positionAnimation, forKey: "bounds")
You will see that that works perfectly. Now change view2 back to textLabel throughout. It still works, it's just that there is nothing to see.
Another way to prove this to yourself is to drop the whole animation and just change the label's layer height:
self.textLabel.layer.bounds.size.height += 50
You will not see anything happen. So there is no bug in your animation; it's all about the way labels are drawn.
You can make the change visible by changing the view instead of the layer:
self.textLabel.bounds.size.height += 50
One additional "problem", though, is that the animation is not animating. Again this is because labels are drawn in a special way.
So whatever it is you're trying to accomplish it, you'll have to do it in a different way. You might have a clear label in front of a colored view and animate the change in height of the view; we've already proven that that works.

Related

Setting the image on UIImageView changes the animation

I have a UIImageView that I wish to animate at the click of a button. But before the animation, I set its image. However, this image-setting messes up the animation. When I comment out the image-set, the animation works as expected.
var busFrame = self.newBusView.frame
let sourceFrame = self.buttonA.superview?.convert(self.buttonA.frame.origin, to: nil)
busFrame.origin.y = sourceFrame!.y
busFrame.origin.x = sourceFrame!.x
self.newBusView.frame = busFrame
//the following line messes up the animation
// self.newBusView.image = UIImage(named: "back_blue")
UIView.animate(withDuration: 2.0, delay: 0.3, options: .curveEaseOut, animations: {
busFrame = self.newBusView.frame
let desstinationFrame = self.buttonD.superview?.convert(self.buttonD.frame.origin, to: nil)
busFrame.origin.y = desstinationFrame!.y
busFrame.origin.x = desstinationFrame!.x
self.newBusView.frame = busFrame
}, completion: { finished in
print("Done!")
//same thing happening here
//self.newBusView.image = UIImage(named: "Background 1")
})
Also note that the buttons are in separate Stack views (hence the need for superview?.convert)
UPDATE:
So it was noticed that whenever I have a button.setImage() the positions go wrong (Even if the button has nothing to do with the animation). But instead of setImage, if I use button.imageView.image = something, then it has no side effects on animations. What is happening?
Expected:
Actual:
As we do not know the "expected" animation, I could not repeat the problem that you have. When I set same image again in the commented line, I get the same animation.
It may be about the area that the image covers. New image's ratio may be different than the default, therefore you see different animation although view's animation is same. You actually animate the view. Hence, you should focus on frames.
I believe that as #musakokcen mentioned your image have different size.
Try to use:
yourImageView.layer.masksToBounds = yes

How to adjust position of CAShapeLayer based upon device size?

I'm attempting to create a CAShapeLayer animation that draws an outline around the frame of a UILabel. Here's the code:
func newQuestionOutline() -> CAShapeLayer {
let outlineShape = CAShapeLayer()
outlineShape.isHidden = false
let circularPath = UIBezierPath(roundedRect: questionLabel.frame, cornerRadius: 5)
outlineShape.path = circularPath.cgPath
outlineShape.fillColor = UIColor.clear.cgColor
outlineShape.strokeColor = UIColor.yellow.cgColor
outlineShape.lineWidth = 5
outlineShape.strokeEnd = 0
view.layer.addSublayer(outlineShape)
return outlineShape
}
func newQuestionAnimation() {
let outlineAnimation = CABasicAnimation(keyPath: "strokeEnd")
outlineAnimation.toValue = 1
outlineAnimation.duration = 5
newQuestionOutline().add(outlineAnimation, forKey: "key")
}
The animation performs as expected when running on the simulator for an iPhone 11 which is the device size that I used in the storyboard. However when running the project on a different device with different screen dimensions (like iPhone 8 plus) the shape is drawn out of place and not around the UILabel as it should be. I used autolayout to horizontally and vertically center the UILabel to the center of the view so the UILabel is centered no matter what device.
Any suggestions? Thanks in advance!
Cheers!
A shape layer is not a view, so it is not subject to auto layout. And any time you say something like roundedRect: questionLabel.frame you are making yourself dependent on what questionLabel.frame is at that moment, which is a huge mistake because that is exactly what is not determined until auto layout determines what the frame will be (and can change later if auto layout changes its mind due to changing conditions, such as rotation etc.)
There are two kinds of solution:
Host the shape layer in a view. Now you have something that is subject to autolayout. You will still need to redraw the shape layer whenever the view changes its frame, but you can detect that and perform the redraw.
Implement your view controller's viewDidLayoutSubviews to detect that auto layout has just done its work. Respond by (for example) removing the shape layer and making a new one based on the current conditions.

How to trigger an animation by PERCENTAGE in swift?

Alright, I am trying to trigger an animation incrementally as a scroll view is scrolled. To do this, I have taken this link here and converted it to swift - http://www.oliverfoggin.com/controlling-animations-with-a-uiscrollview/
Giving me the percentage offset x for my scrollview. This is all great.
Problem is I'm fairly new to swift and don't know how to tie this back into my existing animation which is a transform/move instead of changing color.
Here's my animation here-
self.borderlines.transform = CATransform3DMakeScale(CGFloat(0.01), CGFloat(1.0), 1)
self.activeBorder.layer.opacity = 1
CATransaction.begin()
self.activeBorder.layer.transform = CATransform3DMakeScale(CGFloat(0.03), CGFloat(1.0), 1)
let anim2 = CABasicAnimation(keyPath: "transform")
let fromTransform = CATransform3DMakeScale(CGFloat(0.01), CGFloat(1.0), 1)
let toTransform = CATransform3DMakeScale(CGFloat(1.0), CGFloat(1.0), 1)
anim2.fromValue = NSValue(CATransform3D: fromTransform)
anim2.toValue = NSValue(CATransform3D: toTransform)
anim2.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseOut)
anim2.fillMode = kCAFillModeForwards
anim2.removedOnCompletion = false
self.activeBorder.layer.addAnimation(anim2, forKey: "_activeBorder")
CATransaction.commit()
And then I also have another move animation. As is these are called on touch, so 1 static event.
The guy in the tutorial from what I can see recalculated/triggers his animation EVERY scroll then alters bg color by percentage. I don't know how to apply this to another type of animation- he mentions key frames and I have no idea what those are.
How can I achieve this effect? What do I need to change here?
Look into "freezing" your animation by setting its layer's speed to zero and manipulating its timeOffset as your response to scrolling.

Borders not covering background

I've got a UILabel is using a border the same color as a background which it is half obscuring, to create a nice visual effect. However the problem is that there is still a tiny, yet noticeable, sliver of the label's background color on the OUTSIDE of the border.
The border is not covering the whole label!
Changing the border width doesn't change anything either, sadly.
Here's a picture of what's going on, enlarged so you can see it:
And my code follows:
iconLbl.frame = CGRectMake(theWidth/2-20, bottomView.frame.minY-20, 40, 40)
iconLbl.font = UIFont.fontAwesomeOfSize(23)
iconLbl.text = String.fontAwesomeIconWithName(.Info)
iconLbl.layer.masksToBounds = true
iconLbl.layer.cornerRadius = iconLbl.frame.size.width/2
iconLbl.layer.borderWidth = 5
iconLbl.layer.borderColor = topBackgroundColor.CGColor
iconLbl.backgroundColor = UIColor.cyanColor()
iconLbl.textColor = UIColor.whiteColor()
Is there something I'm missing?
Or am I going to have to figure out another to achieve this effect?
Thanks!
EDIT:
List of things I've tried so far!
Changing layer.borderWidth
Fussing around with clipsToBounds/MasksToBounds
Playing around the the layer.frame
Playing around with an integral frame
EDIT 2:
No fix was found! I used a workaround by extending this method on to my UIViewController
func makeFakeBorder(inputView:UIView,width:CGFloat,color:UIColor) -> UIView {
let fakeBorder = UIView()
fakeBorder.frame = CGRectMake(inputView.frame.origin.x-width, inputView.frame.origin.y-width, inputView.frame.size.width+width*2, inputView.frame.size.height+width*2)
fakeBorder.backgroundColor = color
fakeBorder.clipsToBounds = true
fakeBorder.layer.cornerRadius = fakeBorder.frame.size.width/2
fakeBorder.addSubview(inputView)
inputView.center = CGPointMake(fakeBorder.frame.size.width/2, fakeBorder.frame.size.height/2)
return fakeBorder
}
I believe this is the way a border is drawn to a layer in iOS. In the document it says:
When this value is greater than 0.0, the layer draws a border using the current borderColor value. The border is drawn inset from the receiver’s bounds by the value specified in this property. It is composited above the receiver’s contents and sublayers and includes the effects of the cornerRadius property.
One way to fix this is to apply a mask to a view's layer, but I found out that even if so we still can see a teeny tiny line around the view when doing snapshot tests. So to fix it more, I put this code to layoutSubviews
class MyView: UIView {
override func layoutSubviews() {
super.layoutSubviews()
let maskInset: CGFloat = 1
// Extends the layer's frame.
layer.frame = layer.frame.inset(dx: -maskInset, dy: -maskInset)
// Increase the border width
layer.borderWidth = layer.borderWidth + maskInset
layer.cornerRadius = bounds.height / 2
layer.maskToBounds = true
// Create a circle shape layer with true bounds.
let mask = CAShapeLayer()
mask.path = UIBezierPath(ovalIn: bounds.inset(dx: maskInset, dy: maskInset)).cgPath
layer.mask = mask
}
}
CALayer's mask

Animating a label to increase and decrease in size

I have been trying to implement an animation that brings the users attention to a change in value in a label. I want to do this by quickly increasing and reducing the size of the label (can't think of a better way to describe it) and I've made some progress towards this. The problem is that while the animation increases in size as I want it to; the way it deceases in size isn't smooth. Also, once the animation is complete, the size of the font does not return to the original.
Here is what I have:
func bloat() {
UIView.beginAnimations(nil, context: nil)
UIView.setAnimationDelegate(self)
UIView.setAnimationDelay(0.6)
UIView.setAnimationDuration(0.3)
UIView.setAnimationRepeatCount(4)
UIView.setAnimationCurve(UIViewAnimationCurve.EaseInOut)
currentBudgetDisplay.transform = CGAffineTransformMakeScale(0.9, 0.9)
UIView.commitAnimations()
}
What changes can I make to get it to work as I intend it to?
The requirement is simple using core animation, try this
func bloat() {
var animation = CABasicAnimation(keyPath: "transform.scale")
animation.toValue = NSNumber(float: 0.9)
animation.duration = 0.3
animation.repeatCount = 4.0
animation.autoreverses = true
currentBudgetDisplay.layer.addAnimation(animation, forKey: nil)
}
In iOS 6 and 7, transform UIView animations applied to UIViews under auto layout do not work (because the auto layout stops them). This is fixed in iOS 8, however.
If you insist on supporting iOS 7, there are many workarounds, including using CABasicAnimation (layer animation) instead of UIView animation. See also my essay here: How do I adjust the anchor point of a CALayer, when Auto Layout is being used?