Is there any way to animate a height constraint using an easing curve? I'm thinking of maybe some way to set up a CAKeyFrameAnimation with the values and timings I'd like, and somehow have it affect the height constraint.
I'm not sure if there's a separate animation calss for constraints that I can utilize for constraint keyFrames, or if there's something I'm missing with CAKeyFrameAnimation that would allow me to use that class, or if it's not possible.
Edit: Here's what I'm trying to make work for me, if anyone knows if I'm on the right path or not I'd appreciate some guidance:
self.heightConstraint.constant = newHeight
let animation = CAKeyframeAnimation(keyPath: "frame.size.height")
animation.values = [self.frame.size.height, newHeight]
animation.keyTimes = [0, 1]
animation.duration = self.animationDuration
animation.delegate = self
self.layer.add(animation, forKey: "heightChange")
newHeight would be whatever I want the height to be. But this just pops the view to be taller without any animation. Am I using this correctly? Is this possible to do with constraints?
Edit 2: I should add, I want to use more complex easing functions than the defaults Apple provides as part of UIView.animate(withDuraiton:....
There is no need to use CAKeyframeAnimation. A UIView animation will do easing on animations to a constraint, just like any other animation:
myConstraintOutlet.constant = someNewValue
UIView.animate(
duration: 0.5,
delay: 0.0,
options: .curveEaseInOut, //Use ease-in, ease-out timing.
animations: {
self.view.layoutIfNeeded()
},
completion: nil)
Edit:
If you want custom timing, you could also use the UIView-based keyframe animation with the method animateKeyframes(withDuration:delay:options:animations:completion:) (UIView animation is a whole lot easier to use than CAAnimations.)
Edit #2:
Or, if all you need is a different cubic easing curve, you can do that too. See the very last bit at this link: https://medium.com/#RobertGummesson/a-look-at-uiview-animation-curves-part-3-edde651b6a7a
The key bit is this code snippet from that link:
circleView.transform = CGAffineTransformMakeScale(0, 0)
let timingFunction = CAMediaTimingFunction(controlPoints: 5/6, 0.2, 2/6, 0.9)
CATransaction.begin()
CATransaction.setAnimationTimingFunction(timingFunction)
UIView.animateWithDuration(1) {
self.circleView.transform = CGAffineTransformIdentity
}
CATransaction.commit()
(That's not my code, it's Robert Gummesson's, from the link above. All credit for writing it goes to him.)
Related
I have a button in my iOS app and I want to apply a checkmark animation when the user taps it, but I don't have any idea how would I achieve this. If anybody has an idea please help me out.
Below is a sample gif image of the animation I want to create.
To create that animation you can either animate a mask revealing the check mark from left to right, as Matt says, or you can use a CAShapeLayer to draw the check-mark, and then animate the strokeEnd property of the shape layer.
(The sliding mask approach would always reveal the view's contents from left to right¹, whether the view contained a checkmark or a picture of a kitten. The shape layer stroke approach is specific to drawing shapes using UIBezierPath (or the Core Foundation equivalent, CGPath), but you can use it to draw complex shapes from beginning to end, even shapes that loop back on themselves. It's not limited to always revealing the view content from left to right.)
As it happens, the image you used in your question is the product of a demo app I created last weekend that uses the CAShapeLayer approach.
You can find the complete demo app here:
https://github.com/DuncanMC/AnimateCheckMark.git
In order to do animate the stroke, you'll need to create your check mark as a shape layer. Sample code to create a check mark looks like this:
private func checkmarkPath() -> CGPath {
let path = UIBezierPath()
path.move(to: CGPoint(x: 5, y: bounds.midY))
path.addLine(to: CGPoint(x: 25, y: bounds.midY + 20))
path.addLine(to: CGPoint(x: 45, y: bounds.midY - 20))
return path.cgPath
}
(That code isn't very general-purpose. It creates a fixed sized check mark. You might need to modify it to scale the check-mark to fit the view it belongs to.)
Once you have created a shape layer and installed your checkmark in it you need to create an animation that animates the shape layer's strokeEnd property. That code might look like this:
This function animates showing or hiding the checkmark view's layer by animating the layer's strokeEnd property.
- Parameter show: If true, show the checkmark. If false, hide it.
- Parameter animationDuration: the duration of the animation, in seconds.
- Parameter animationCurve: Indicates the animation timing curve to use, .linear or .easeInEaseOut
*/
public func animateCheckmarkStrokeEnd(_ show: Bool,
animationDuration: Double = 0.3,
animationCurve: AnimationCurve = .linear) {
guard let layer = layer as? CAShapeLayer else { return }
let newStrokeEnd: CGFloat = show ? 1.0 : 0.0
let oldStrokeEnd: CGFloat = show ? 0.0 : 1.0
let keyPath = "strokeEnd"
let animation = CABasicAnimation(keyPath: keyPath)
animation.fromValue = oldStrokeEnd
animation.toValue = newStrokeEnd
animation.duration = animationDuration
let timingFunction: CAMediaTimingFunction
if animationCurve == .linear {
timingFunction = CAMediaTimingFunction(name: CAMediaTimingFunctionName.linear)
} else {
timingFunction = CAMediaTimingFunction(name: CAMediaTimingFunctionName.easeInEaseOut)
}
animation.timingFunction = timingFunction
layer.add(animation, forKey: nil)
DispatchQueue.main.async {
layer.strokeEnd = newStrokeEnd
}
self.checked = show
}
The entire demo app lets you create the checkmark animation as either a stroke animation or a cross-fade, and allows you to use either linear timing or ease-in/ease-out timing. The window for the demo app looks like this:
¹: You could create other mask animations that reveal your view's contents from right to left, top to bottom, from the center out, etc. For the checkmark animation in your question, though, the mask animation you would need would be a left-to-right "wipe" animation.
I'm trying to add this 3D animation but it's not animating it properly. There is a jerk during the animation.
var currentTransform = CATransform3DIdentity
UIView.animate(withDuration: 0.3, animations: {
currentTransform = CATransform3DMakeScale(0.5, 0.5, 1)
currentTransform.m34 = 1.0 / -800
currentTransform = CATransform3DRotate(currentTransform, CGFloat(M_PI) * 50.0/180.0, 0, 1, 0)
self.containerView.layer.transform = currentTransform
})
What can be the problem and how to solve it?
UPDATE (Answer):
This piece of code works fine. The only problem was, the containerView was an immediate subView of my main view and this was causing the jerk.
The solution to this is, I added another view to my main view and made the containerView a subview of the newly added view.
Though I'm not sure why this happened. I think, the transformation I was doing on my containerView, also affected the layer of the main view through transformation.
For some reason, when I try to animate textColor, it won't work. The textColor just suddenly changes from A to B. Is it possible to animate it, for example, red to black?
Instead, have you tried using a crossfade transition on the object itself like this, it'll give you a nice fade-in fade-out effect from one color to another:
Objective C
[UIView transitionWithView:myLabel duration:0.25 options:UIViewAnimationOptionTransitionCrossDissolve animations:^{
myLabel.textColor = NEW_COLOR;
} completion:^(BOOL finished) {
}];
Swift 5
UIView.transition(with: creditsLabel, duration: 0.25, options: .transitionCrossDissolve) {
self.creditsLabel.textColor = .red
}
This is better than using NSTimers, CATextLayers and so on so forth for various reasons. CATextLayer does not have proper support for text kerning or NSAttributedText, and NSTimers are laggy (plus there's too much code). The transition animation above does the trick, and also can be used in a chain animation. I had the same issue and have already tried the solutions above but this simple code works wonders instead.
Swift 4 solution:
UIView.transition(with: yourLabel, duration: 0.3, options: .transitionCrossDissolve, animations: {
self.yourLabel.textColor = .red
}, completion: nil)
The reason that textColor is not animatable is that UILabel uses a regular CALayer instead of a CATextLayer.
To make textColor animatable (as well as text, font, etc.) we can subclass UILabel to make it use a CATextLayer.
This is quite a lot of work, but luckily I already did it :-)
You can find a complete explanation + a drop-in open source replacement for UILabel in this article
You could try creating another instance of the UILabel or whatever it is that has the textColor, and then apply the animation between those two instances (the with the old textColor and the one with the new textColor).
This was the ONLY thing that worked for me :
let changeColor = CATransition()
changeColor.duration = 1
CATransaction.begin()
CATransaction.setCompletionBlock {
selected.label.layer.add(changeColor, forKey: nil)
selected.label.textColor = .black
}
selected.nameLabel.textColor = .red
CATransaction.commit()
This answer is obsolete, and is not a good solution for the original question. #strange 's answer below is much better and should be used instead of this answer: https://stackoverflow.com/a/20892927/76559
//Old answer below
The textColor property is not specified as being animatable in the docs, so I don't think you can do it with a simple UIView animations block...
This could probably be done pretty crudely with an NSTimer firing every couple of milliseconds, each time setting the colour gradually from one to the other.
I say this is crude because it would require an array or some other container of preset colour values going from the start colour to the finish colour, and I'm sure there's a way you could do this using core animation or something, I just don't know what it is.
Here's my code to animate label text color. The important part is to set textColor to clearColor before animation
label.textColor = [UIColor clearColor];
[UIView transitionWithView:label duration:duration/4 options:UIViewAnimationOptionTransitionCrossDissolve animations:^{
label.textColor = self.highlightedCellPrimaryTextColor;
} completion:^(BOOL finished) { }];
I found it pretty easy placing an UIView below the label and animating its opacity.
The label has to be added to that view.
Maybe not a good solution from the resources consumption point of view, but pretty straightforward.
updated swift 3.0
I just changed from #budidino comments
UIView.transition(with: my.label, duration: 0.3, options: .transitionCrossDissolve, animations: { my.label.textColor = .black }, completion: nil)
Thanks for #Strange's answer, working perfectly in Swift 5, Xcode 11 with a bit syntax change. I was animating text color for multiple UILabels, if this is also your case, try this
for label in [label1, label2, ...] as [UILabel] { // loop through a #IBOutlet UILabel array
UIView.transition(with: label, duration: 0.3, options: .transitionCrossDissolve, animations: {
label.textColor = .label
}, completion: nil)
}
Try using core animation
func textColorBlinking(){
let changeColor = CATransition()
changeColor.duration = 1
changeColor.type = .fade
changeColor.repeatCount = Float.infinity
CATransaction.begin()
CATransaction.setCompletionBlock {
self.lblHome.layer.add(changeColor, forKey: nil)
self.lblHome.textColor = .white
}
self.lblHome.textColor = .red
CATransaction.commit()
}
How do I programmatically via Quartz, animate a rectangle from lying face up (appear as line in 2D) to full height?
The following (pardon the crude drawing) is what I'm trying to get: a deck of cards (lines) with a card pivoting to full height. I don't have any means of adjusting for perspective.
Possible modus operandi: 1) start off with a UIImageView having zero height. 2) Upper (xl,yl)(xr,yr) coordinates widening apart (adjusting perspective) as the height increases.
Any reference, API suggestions welcomed.
This will be relatively close to your desired animation with examples for both UIView animations and CABasicAnimation.
To begin, let's set up the from/to 3D transformations:
let perspective: CGFloat = 1.0 / 1000.0
var fromTransform = CATransform3DMakeRotation(-CGFloat(M_PI_2), 1, 0, 0)
fromTransform.m34 = perspective
var toTransform = CATransform3DMakeRotation(0, 1, 0, 0)
toTransform.m34 = perspective
To animate with UIView animations:
view.layer.transform = fromTransform
UIView.animateWithDuration(1.0, animations: {
view.layer.transform = toTransform
})
If you want to use CABasicAnimation:
let flipAnimation = CABasicAnimation(keyPath: "transform")
flipAnimation.fromValue = NSValue(CATransform3D: fromTransform)
flipAnimation.toValue = NSValue(CATransform3D: toTransform)
flipAnimation.duration = 1.0
flipAnimation.fillMode = kCAFillModeForwards
view.layer.addAnimation(flipAnimation, forKey: "flip")
Edit:
OP desires the anchor point of the animation to be bottom-center, this can be achieved by:
view.layer.anchorPoint = CGPointMake(0.5, 1.0)
For some reason, when I try to animate textColor, it won't work. The textColor just suddenly changes from A to B. Is it possible to animate it, for example, red to black?
Instead, have you tried using a crossfade transition on the object itself like this, it'll give you a nice fade-in fade-out effect from one color to another:
Objective C
[UIView transitionWithView:myLabel duration:0.25 options:UIViewAnimationOptionTransitionCrossDissolve animations:^{
myLabel.textColor = NEW_COLOR;
} completion:^(BOOL finished) {
}];
Swift 5
UIView.transition(with: creditsLabel, duration: 0.25, options: .transitionCrossDissolve) {
self.creditsLabel.textColor = .red
}
This is better than using NSTimers, CATextLayers and so on so forth for various reasons. CATextLayer does not have proper support for text kerning or NSAttributedText, and NSTimers are laggy (plus there's too much code). The transition animation above does the trick, and also can be used in a chain animation. I had the same issue and have already tried the solutions above but this simple code works wonders instead.
Swift 4 solution:
UIView.transition(with: yourLabel, duration: 0.3, options: .transitionCrossDissolve, animations: {
self.yourLabel.textColor = .red
}, completion: nil)
The reason that textColor is not animatable is that UILabel uses a regular CALayer instead of a CATextLayer.
To make textColor animatable (as well as text, font, etc.) we can subclass UILabel to make it use a CATextLayer.
This is quite a lot of work, but luckily I already did it :-)
You can find a complete explanation + a drop-in open source replacement for UILabel in this article
You could try creating another instance of the UILabel or whatever it is that has the textColor, and then apply the animation between those two instances (the with the old textColor and the one with the new textColor).
This was the ONLY thing that worked for me :
let changeColor = CATransition()
changeColor.duration = 1
CATransaction.begin()
CATransaction.setCompletionBlock {
selected.label.layer.add(changeColor, forKey: nil)
selected.label.textColor = .black
}
selected.nameLabel.textColor = .red
CATransaction.commit()
This answer is obsolete, and is not a good solution for the original question. #strange 's answer below is much better and should be used instead of this answer: https://stackoverflow.com/a/20892927/76559
//Old answer below
The textColor property is not specified as being animatable in the docs, so I don't think you can do it with a simple UIView animations block...
This could probably be done pretty crudely with an NSTimer firing every couple of milliseconds, each time setting the colour gradually from one to the other.
I say this is crude because it would require an array or some other container of preset colour values going from the start colour to the finish colour, and I'm sure there's a way you could do this using core animation or something, I just don't know what it is.
Here's my code to animate label text color. The important part is to set textColor to clearColor before animation
label.textColor = [UIColor clearColor];
[UIView transitionWithView:label duration:duration/4 options:UIViewAnimationOptionTransitionCrossDissolve animations:^{
label.textColor = self.highlightedCellPrimaryTextColor;
} completion:^(BOOL finished) { }];
I found it pretty easy placing an UIView below the label and animating its opacity.
The label has to be added to that view.
Maybe not a good solution from the resources consumption point of view, but pretty straightforward.
updated swift 3.0
I just changed from #budidino comments
UIView.transition(with: my.label, duration: 0.3, options: .transitionCrossDissolve, animations: { my.label.textColor = .black }, completion: nil)
Thanks for #Strange's answer, working perfectly in Swift 5, Xcode 11 with a bit syntax change. I was animating text color for multiple UILabels, if this is also your case, try this
for label in [label1, label2, ...] as [UILabel] { // loop through a #IBOutlet UILabel array
UIView.transition(with: label, duration: 0.3, options: .transitionCrossDissolve, animations: {
label.textColor = .label
}, completion: nil)
}
Try using core animation
func textColorBlinking(){
let changeColor = CATransition()
changeColor.duration = 1
changeColor.type = .fade
changeColor.repeatCount = Float.infinity
CATransaction.begin()
CATransaction.setCompletionBlock {
self.lblHome.layer.add(changeColor, forKey: nil)
self.lblHome.textColor = .white
}
self.lblHome.textColor = .red
CATransaction.commit()
}