I'm animating show and hide of my FAB button, however I want to prevent showing FAB if it is already shown. How to read current layer scale?
func hideFab(){
let materialCurve = MDCAnimationTimingFunction.deceleration
let timingFunction = CAMediaTimingFunction.mdc_function(withType: materialCurve)
let animation = CABasicAnimation(keyPath:"transform.scale.xy")
animation.timingFunction = timingFunction
animation.fromValue = 1
animation.toValue = 0
animation.duration = 0.5
animation.isRemovedOnCompletion = false
animation.fillMode = .forwards
fab.layer.playAnimation({ (layer) -> CAAnimation in
animation
}, key: animation.keyPath!)
}
func showFab(){
let materialCurve = MDCAnimationTimingFunction.deceleration
let timingFunction = CAMediaTimingFunction.mdc_function(withType: materialCurve)
let animation = CABasicAnimation(keyPath:"transform.scale.xy")
animation.timingFunction = timingFunction
animation.fromValue = 0
animation.toValue = 1
animation.duration = 0.5
animation.isRemovedOnCompletion = false
animation.fillMode = .forwards
fab.layer.playAnimation({ (layer) -> CAAnimation in
animation
}, key: animation.keyPath!)
}
With this let currentScale = fab.layer.presentation()?.value(forKeyPath: "transform.scale.xy") ?? 0.0 you should be able to get the presentation scale value.
But I'd try to instead check if I showed or not the FAB with a tag:
let hiddenFABTag = 99
let shownFABTag = 100
func hideFAB(){
if fab.tag == shownFABTag {
...
...
fab.tag = hiddenFABTag
}
}
func showFAB(){
if fab.tag == hiddenFABTag {
...
...
fab.tag = shownFABTag
}
}
The first time you call it, either in viewDidLoad() or somewhere, you might want to give it a default tag value, if you know if it's going to be shown or hidden the first time by saying:
override func viewDidLoad() {
super.viewDidLoad()
fab.tag = hiddenFABTag
}
I want to re-animate gradient background with different color when animating.
I can successfully animate gradient color with this code.
let dayTopColor = CommonUtils.colorWithHexString("955EAC")
let dayBottomColor = CommonUtils.colorWithHexString("9F3050")
let dayToTopColor = CommonUtils.colorWithHexString("D15B52")
let dayToBottomColor = CommonUtils.colorWithHexString("CC4645")
let nightTopColor = CommonUtils.colorWithHexString("2D5E7C")
let nightBottomColor = CommonUtils.colorWithHexString("19337D")
let nightToTopColor = CommonUtils.colorWithHexString("21334E")
let nightToBottomColor = CommonUtils.colorWithHexString("101A55")
var isInSaudiArabia = false
var gradient : CAGradientLayer?
var toColors : AnyObject?
var fromColors : AnyObject?
func animateBackground(){
var layerToRemove: CAGradientLayer?
for layer in self.view.layer.sublayers!{
if layer.isKindOfClass(CAGradientLayer) {
layerToRemove = layer as? CAGradientLayer
}
}
layerToRemove?.removeFromSuperlayer()
self.gradient!.colors = [nightTopColor.CGColor, nightBottomColor.CGColor]
self.toColors = [nightToTopColor.CGColor, nightToBottomColor.CGColor]
self.view.layer.insertSublayer(self.gradient!, atIndex: 0)
animateLayer()
}
func animateLayer(){
self.fromColors = self.gradient!.colors!
self.gradient!.colors = self.toColors as? [AnyObject]
let animation : CABasicAnimation = CABasicAnimation(keyPath: "colors")
animation.delegate = self
animation.fromValue = fromColors
animation.toValue = toColors
animation.duration = 3.50
animation.removedOnCompletion = true
animation.fillMode = kCAFillModeForwards
animation.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionLinear)
animation.delegate = self
self.gradient!.addAnimation(animation, forKey:"animateGradient")
}
override func animationDidStop(anim: CAAnimation, finished flag: Bool) {
self.toColors = self.fromColors;
self.fromColors = self.gradient!.colors!
animateLayer()
}
CommonUtils.colorWithHexString() is a function which converts hex color to UIColor.
Btw when i try to change background color to day color while animating, background gradient color got flickered.
Is there anybody who knows solution.
The problem is that when you remove the layer, it stops the animation. But when the animation stops, animationDidStop is still getting called, which is starting a new animation, itself. So, you're removing the layer, which stops the animation, immediately starts another, but you're then starting yet another animation. You have dueling animations.
You can check flag to see if the animation finished properly before animationDidStop should call animateLayer.
override func animationDidStop(anim: CAAnimation, finished flag: Bool) {
if flag {
toColors = fromColors;
fromColors = gradient!.colors!
animateLayer()
}
}
Personally, I'm not sure why you're removing and adding and removing the layer. And if you were, I'm not sure why you don't just gradient?.removeFromSuperlayer() rather than iterating through the layers.
Regardless, I'd just keep the gradient layer there, just check its presentationLayer and start the animation from there:
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
fromColors = [dayTopColor.CGColor, dayBottomColor.CGColor]
toColors = [dayToTopColor.CGColor, dayToBottomColor.CGColor]
gradient = CAGradientLayer()
gradient!.colors = fromColors!
gradient!.frame = view.bounds
view.layer.addSublayer(gradient!)
animateLayer()
}
let dayTopColor = CommonUtils.colorWithHexString("955EAC")
let dayBottomColor = CommonUtils.colorWithHexString("9F3050")
let dayToTopColor = CommonUtils.colorWithHexString("D15B52")
let dayToBottomColor = CommonUtils.colorWithHexString("CC4645")
let nightTopColor = CommonUtils.colorWithHexString("2D5E7C")
let nightBottomColor = CommonUtils.colorWithHexString("19337D")
let nightToTopColor = CommonUtils.colorWithHexString("21334E")
let nightToBottomColor = CommonUtils.colorWithHexString("101A55")
var gradient : CAGradientLayer?
var toColors : [CGColor]?
var fromColors : [CGColor]?
var day = true
func toggleFromDayToNight() {
day = !day
if day {
fromColors = [dayTopColor.CGColor, dayBottomColor.CGColor]
toColors = [dayToTopColor.CGColor, dayToBottomColor.CGColor]
} else {
fromColors = [nightTopColor.CGColor, nightBottomColor.CGColor]
toColors = [nightToTopColor.CGColor, nightToBottomColor.CGColor]
}
let colors = (gradient!.presentationLayer() as! CAGradientLayer).colors // save the in-flight current colors
gradient!.removeAnimationForKey("animateGradient") // cancel the animation
gradient!.colors = colors // restore the colors to in-flight values
animateLayer() // start animation
}
func animateLayer() {
let animation : CABasicAnimation = CABasicAnimation(keyPath: "colors")
animation.fromValue = gradient!.colors
animation.toValue = toColors
animation.duration = 3.50
animation.removedOnCompletion = true
animation.fillMode = kCAFillModeForwards
animation.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionLinear)
animation.delegate = self
gradient!.colors = toColors
gradient!.addAnimation(animation, forKey:"animateGradient")
}
override func animationDidStop(anim: CAAnimation, finished flag: Bool) {
if flag {
swap(&toColors, &fromColors)
animateLayer()
}
}
#IBAction func didTapButton() {
toggleFromDayToNight()
}
}
I have a custom UIActivityIndicatorView. It is a view, with a CAAnimation on its layer. The problem is the following:
I do some heavy work and create a lot of views. It takes approximately 0.5 seconds. In order for it to be smooth I decided to use activity indicator, while it "happens". It was all fine with default activity indicator, however with the one that I wrote I get unexpected results.
So, when the view loads I launch my activity indicator, which starts animating. When heavy duty work starts my view freezes for 0.5 seconds and when it's done I stop animating it and it disappears. This "freeze" looks very unpleasant to an eye. Because the idea was to keep animating while other views get initialized and added as subviews(although hidden).
I suspect that the problem is that my "activity indicator" is not asynchronous or simply was not coded right.
Here is the code for it:
class CustomActivityIndicatorView: UIView {
// MARK - Variables
var colors = [UIColor.greenColor(),UIColor.grayColor(),UIColor.blueColor(),UIColor.redColor()]
var colorIndex = 0
var animation: CABasicAnimation!
lazy var customView : UIView! = {
let frame : CGRect = CGRectMake(0.0, 0.0, 100, 100)
let view = UIView(frame: frame)
image.frame = frame
image.center = view.center
view.backgroundColor = UIColor.greenColor()
view.clipsToBounds = true
view.layer.cornerRadius = frame.width/2
return view
}()
var isAnimating : Bool = false
var hidesWhenStopped : Bool = true
var from: NSNumber = 1.0
var to: NSNumber = 0.0
var growing = false
override func animationDidStart(anim: CAAnimation!) {
}
override func animationDidStop(anim: CAAnimation!, finished flag: Bool) {
growing = !growing
if growing {
colorIndex++
if colorIndex == colors.count {
colorIndex = 0
}
println(colorIndex)
customView.backgroundColor = colors[colorIndex]
from = 0.0
to = 1.0
} else {
from = 1.0
to = 0.0
}
if isAnimating {
addPulsing()
resume()
}
}
// MARK - Init
required init(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
self.addSubview(customView)
addPulsing()
pause()
self.hidden = true
}
// MARK - Func
func addPulsing() {
let pulsing : CABasicAnimation = CABasicAnimation(keyPath: "transform.scale")
pulsing.duration = 0.4
pulsing.removedOnCompletion = false
pulsing.fillMode = kCAFillModeForwards
pulsing.fromValue = from
pulsing.toValue = to
pulsing.delegate = self
let layer = customView.layer
layer.addAnimation(pulsing, forKey: "pulsing")
}
func pause() {
let layer = customView.layer
let pausedTime = layer.convertTime(CACurrentMediaTime(), fromLayer: nil)
layer.speed = 0.0
layer.timeOffset = pausedTime
isAnimating = false
}
func resume() {
let layer = customView.layer
let pausedTime : CFTimeInterval = layer.timeOffset
layer.speed = 1.0
layer.timeOffset = 0.0
layer.beginTime = 0.0
let timeSincePause = layer.convertTime(CACurrentMediaTime(), fromLayer: nil) - pausedTime
layer.beginTime = timeSincePause
isAnimating = true
}
func startAnimating () {
if isAnimating {
return
}
if hidesWhenStopped {
self.hidden = false
}
resume()
}
func stopAnimating () {
let layer = customView.layer
if hidesWhenStopped {
self.hidden = true
}
pause()
layer.removeAllAnimations()
}
deinit {
println("Spinner Deinitied")
}
}
Regarding animationDidStop method:
The idea is the following. The view pulsates, and after it has shrunk, it starts growing again and the background color is changed.
Any idea on what I'm doing wrong?
Solved it using CAKeyFrameAnimation to achieve the same effect. For everybody with the same problem, remember that animationDidStart and animationDidStop start running on the main thread, so that whatever you do with your animation there will be halted if the main thread is busy.
I am animating a view and I want to pause it and resume it.
Using an apple guide I created a CALayer Extension
extension CALayer {
func pause() {
var pauseTime = self.convertTime(CACurrentMediaTime(), fromLayer: nil)
self.speed = 0.0
self.timeOffset = pauseTime
}
func resume() {
var pausedTime = self.timeOffset
self.speed = 1.0
self.timeOffset = 0.0
self.beginTime = 0.0
var timeSincePause = self.convertTime(CACurrentMediaTime(), toLayer: nil) - pausedTime
self.beginTime = timeSincePause
}
}
This code is working perfectly except when that app goes to background. When I bring the App back to foreground animations is finished (even if the time is not pass) and it is not starting again when I click resume.
Ok. I tried animating CALayer but I have the same problem.
extension CALayer {
func animateY(newY:CGFloat,time:NSTimeInterval,completion:()->Void){
CATransaction.begin()
CATransaction.setCompletionBlock(completion)
let animation = CABasicAnimation(keyPath: "position.y")
animation.fromValue = self.position.y
animation.toValue = newY
animation.duration = time
animation.delegate = self
animation.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseOut)
animation.removedOnCompletion = false // don't remove after finishing
self.position.y = newY
self.addAnimation(animation, forKey: "position.y")
CATransaction.flush()
}
}
I recommend using CABasicAnimation. Your resume/pause methods should be fine, since they are from this answer. You should try using Core Animation instead of UIViewAnimation and then the resume/pause will work.
Then you can register for the two notifications UIApplicationWillEnterForegroundNotification and UIApplicationDidEnterBackgroundNotification to have full control over the pause/resume actions.
I'm setting a new text value to a UILabel. Currently, the new text appears just fine. However, I'd like to add some animation when the new text appears. I'm wondering what I can do to animate the appearance of the new text.
I wonder if it works, and it works perfectly!
Objective-C
[UIView transitionWithView:self.label
duration:0.25f
options:UIViewAnimationOptionTransitionCrossDissolve
animations:^{
self.label.text = rand() % 2 ? #"Nice nice!" : #"Well done!";
} completion:nil];
Swift 3, 4, 5
UIView.transition(with: label,
duration: 0.25,
options: .transitionCrossDissolve,
animations: { [weak self] in
self?.label.text = (arc4random()() % 2 == 0) ? "One" : "Two"
}, completion: nil)
Objective-C
To achieve a true cross-dissolve transition (old label fading out while new label fading in), you don't want fade to invisible. It would result in unwanted flicker even if text is unchanged.
Use this approach instead:
CATransition *animation = [CATransition animation];
animation.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut];
animation.type = kCATransitionFade;
animation.duration = 0.75;
[aLabel.layer addAnimation:animation forKey:#"kCATransitionFade"];
// This will fade:
aLabel.text = "New"
Also see:
Animate UILabel text between two numbers?
Demonstration in iOS 10, 9, 8:
Tested with Xcode 8.2.1 & 7.1, ObjectiveC on iOS 10 to 8.0.
► To download the full project, search for SO-3073520 in Swift Recipes.
Swift 4
The proper way to fade a UILabel (or any UIView for that matter) is to use a Core Animation Transition. This will not flicker, nor will it fade to black if the content is unchanged.
A portable and clean solution is to use a Extension in Swift (invoke prior changing visible elements)
// Usage: insert view.fadeTransition right before changing content
extension UIView {
func fadeTransition(_ duration:CFTimeInterval) {
let animation = CATransition()
animation.timingFunction = CAMediaTimingFunction(name:
CAMediaTimingFunctionName.easeInEaseOut)
animation.type = CATransitionType.fade
animation.duration = duration
layer.add(animation, forKey: CATransitionType.fade.rawValue)
}
}
Invocation looks like this:
// This will fade
aLabel.fadeTransition(0.4)
aLabel.text = "text"
► Find this solution on GitHub and additional details on Swift Recipes.
since iOS4 it can be obviously done with blocks:
[UIView animateWithDuration:1.0
animations:^{
label.alpha = 0.0f;
label.text = newText;
label.alpha = 1.0f;
}];
Here is the code to make this work.
[UIView beginAnimations:#"animateText" context:nil];
[UIView setAnimationCurve:UIViewAnimationCurveEaseIn];
[UIView setAnimationDuration:1.0f];
[self.lbl setAlpha:0];
[self.lbl setText:#"New Text";
[self.lbl setAlpha:1];
[UIView commitAnimations];
Swift 4.2 version of SwiftArchitect's solution above (works great):
// Usage: insert view.fadeTransition right before changing content
extension UIView {
func fadeTransition(_ duration:CFTimeInterval) {
let animation = CATransition()
animation.timingFunction = CAMediaTimingFunction(name: CAMediaTimingFunctionName.easeInEaseOut)
animation.type = CATransitionType.fade
animation.duration = duration
layer.add(animation, forKey: CATransitionType.fade.rawValue)
}
}
Invocation:
// This will fade
aLabel.fadeTransition(0.4)
aLabel.text = "text"
UILabel Extension Solution
extension UILabel{
func animation(typing value:String,duration: Double){
let characters = value.map { $0 }
var index = 0
Timer.scheduledTimer(withTimeInterval: duration, repeats: true, block: { [weak self] timer in
if index < value.count {
let char = characters[index]
self?.text! += "\(char)"
index += 1
} else {
timer.invalidate()
}
})
}
func textWithAnimation(text:String,duration:CFTimeInterval){
fadeTransition(duration)
self.text = text
}
//followed from #Chris and #winnie-ru
func fadeTransition(_ duration:CFTimeInterval) {
let animation = CATransition()
animation.timingFunction = CAMediaTimingFunction(name:
CAMediaTimingFunctionName.easeInEaseOut)
animation.type = CATransitionType.fade
animation.duration = duration
layer.add(animation, forKey: CATransitionType.fade.rawValue)
}
}
Simply Called function by
uiLabel.textWithAnimation(text: "text you want to replace", duration: 0.2)
Thanks for all the tips guys. Hope this will help in long term
With Swift 5, you can choose one of the two following Playground code samples in order to animate your UILabel's text changes with some cross dissolve animation.
#1. Using UIView's transition(with:duration:options:animations:completion:) class method
import UIKit
import PlaygroundSupport
class ViewController: UIViewController {
let label = UILabel()
override func viewDidLoad() {
super.viewDidLoad()
label.text = "Car"
view.backgroundColor = .white
view.addSubview(label)
label.translatesAutoresizingMaskIntoConstraints = false
label.centerXAnchor.constraint(equalTo: view.centerXAnchor).isActive = true
label.centerYAnchor.constraint(equalTo: view.centerYAnchor).isActive = true
let tapGesture = UITapGestureRecognizer(target: self, action: #selector(toggle(_:)))
view.addGestureRecognizer(tapGesture)
}
#objc func toggle(_ sender: UITapGestureRecognizer) {
let animation = {
self.label.text = self.label.text == "Car" ? "Plane" : "Car"
}
UIView.transition(with: label, duration: 2, options: .transitionCrossDissolve, animations: animation, completion: nil)
}
}
let controller = ViewController()
PlaygroundPage.current.liveView = controller
#2. Using CATransition and CALayer's add(_:forKey:) method
import UIKit
import PlaygroundSupport
class ViewController: UIViewController {
let label = UILabel()
let animation = CATransition()
override func viewDidLoad() {
super.viewDidLoad()
label.text = "Car"
animation.timingFunction = CAMediaTimingFunction(name: CAMediaTimingFunctionName.easeInEaseOut)
// animation.type = CATransitionType.fade // default is fade
animation.duration = 2
view.backgroundColor = .white
view.addSubview(label)
label.translatesAutoresizingMaskIntoConstraints = false
label.centerXAnchor.constraint(equalTo: view.centerXAnchor).isActive = true
label.centerYAnchor.constraint(equalTo: view.centerYAnchor).isActive = true
let tapGesture = UITapGestureRecognizer(target: self, action: #selector(toggle(_:)))
view.addGestureRecognizer(tapGesture)
}
#objc func toggle(_ sender: UITapGestureRecognizer) {
label.layer.add(animation, forKey: nil) // The special key kCATransition is automatically used for transition animations
label.text = label.text == "Car" ? "Plane" : "Car"
}
}
let controller = ViewController()
PlaygroundPage.current.liveView = controller
Swift 2.0:
UIView.transitionWithView(self.view, duration: 1.0, options: UIViewAnimationOptions.TransitionCrossDissolve, animations: {
self.sampleLabel.text = "Animation Fade1"
}, completion: { (finished: Bool) -> () in
self.sampleLabel.text = "Animation Fade - 34"
})
OR
UIView.animateWithDuration(0.2, animations: {
self.sampleLabel.alpha = 1
}, completion: {
(value: Bool) in
self.sampleLabel.alpha = 0.2
})
The animation's duration and timingFunction properties can be omitted, in which case they will take their default values of 0.25 and .curveEaseInEaseOut, respectively.
let animation = CATransition()
label.layer.add(animation, forKey: nil)
label.text = "New text"
is the same as writing this:
let animation = CATransition()
animation.duration = 0.25
animation.timingFunction = .curveEaseInEaseOut
label.layer.add(animation, forKey: nil)
label.text = "New text"
Swift 4.2 solution (taking 4.0 answer and updating for new enums to compile)
extension UIView {
func fadeTransition(_ duration:CFTimeInterval) {
let animation = CATransition()
animation.timingFunction = CAMediaTimingFunction(name:
CAMediaTimingFunctionName.easeInEaseOut)
animation.type = CATransitionType.fade
animation.duration = duration
layer.add(animation, forKey: CATransitionType.fade.rawValue)
}
}
func updateLabel() {
myLabel.fadeTransition(0.4)
myLabel.text = "Hello World"
}
There is one more solution to achieve this. It was described here. The idea is subclassing UILabel and overriding action(for:forKey:) function in the following way:
class LabelWithAnimatedText: UILabel {
override var text: String? {
didSet {
self.layer.setValue(self.text, forKey: "text")
}
}
override func action(for layer: CALayer, forKey event: String) -> CAAction? {
if event == "text" {
if let action = self.action(for: layer, forKey: "backgroundColor") as? CAAnimation {
let transition = CATransition()
transition.type = kCATransitionFade
//CAMediatiming attributes
transition.beginTime = action.beginTime
transition.duration = action.duration
transition.speed = action.speed
transition.timeOffset = action.timeOffset
transition.repeatCount = action.repeatCount
transition.repeatDuration = action.repeatDuration
transition.autoreverses = action.autoreverses
transition.fillMode = action.fillMode
//CAAnimation attributes
transition.timingFunction = action.timingFunction
transition.delegate = action.delegate
return transition
}
}
return super.action(for: layer, forKey: event)
}
}
Usage examples:
// do not forget to set the "Custom Class" IB-property to "LabelWithAnimatedText"
// #IBOutlet weak var myLabel: LabelWithAnimatedText!
// ...
UIView.animate(withDuration: 0.5) {
myLabel.text = "I am animated!"
}
myLabel.text = "I am not animated!"
This is a C# UIView extension method that's based on #SwiftArchitect's code. When auto layout is involved and controls need to move depending on the label's text, this calling code uses the Superview of the label as the transition view instead of the label itself. I added a lambda expression for the action to make it more encapsulated.
public static void FadeTransition( this UIView AView, double ADuration, Action AAction )
{
CATransition transition = new CATransition();
transition.Duration = ADuration;
transition.TimingFunction = CAMediaTimingFunction.FromName( CAMediaTimingFunction.Linear );
transition.Type = CATransition.TransitionFade;
AView.Layer.AddAnimation( transition, transition.Type );
AAction();
}
Calling code:
labelSuperview.FadeTransition( 0.5d, () =>
{
if ( condition )
label.Text = "Value 1";
else
label.Text = "Value 2";
} );
If you would like to do this in Swift with a delay try this:
delay(1.0) {
UIView.transitionWithView(self.introLabel, duration: 0.25, options: [.TransitionCrossDissolve], animations: {
self.yourLabel.text = "2"
}, completion: { finished in
self.delay(1.0) {
UIView.transitionWithView(self.introLabel, duration: 0.25, options: [.TransitionCrossDissolve], animations: {
self.yourLabel.text = "1"
}, completion: { finished in
})
}
})
}
using the following function created by #matt - https://stackoverflow.com/a/24318861/1982051:
func delay(delay:Double, closure:()->()) {
dispatch_after(
dispatch_time(
DISPATCH_TIME_NOW,
Int64(delay * Double(NSEC_PER_SEC))
),
dispatch_get_main_queue(), closure)
}
which will become this in Swift 3
func delay(_ delay:Double, closure:()->()) {
let when = DispatchTime.now() + delay
DispatchQueue.main.after(when: when, execute: closure)
}