Search code examples
iosswiftanimationuilabelcatransition

UILabel text change animation flickers when one text is longer than other


I am using the ideal solution to animate text changes to my UILabel. Similar to this answer https://stackoverflow.com/a/33705634/8704900
The problem is when the texts are not of same length, the animation is no more smooth and the text flickers before animating up.
Video: https://drive.google.com/file/d/1I89NnzjQp7TbemO-dmcbKzYUr7pM7mGk/view?usp=sharing

My code looks like this:

@IBOutlet weak var label: UILabel!

var titleLabelAnimation: CATransition = {
        let animation = CATransition()
        animation.timingFunction = CAMediaTimingFunction(name: CAMediaTimingFunctionName.default)
        animation.type = .push
        animation.subtype = .fromTop
        animation.duration = 0.5
        return animation
    }()

    @IBAction func didTap() {
        self.label.layer.add(self.titleLabelAnimation, forKey: nil)
        self.label.text = "Can you see a flicker?"
        self.label.sizeToFit()
    }
    
    @IBAction func didTapRev() {
        self.label.layer.add(self.titleLabelAnimation, forKey: nil)
        self.label.text = "Hello this is animation !!!"
        self.label.sizeToFit()
    }

I have tried layoutIfNeeded(), sizeToFit(), changing the text before animation and couple of other workarounds. Nothing seem to be working!


Solution

  • Not sure exactly what is going on in your example as I could not produce this result. But animations are in many cases a pain and a lot of things may produce jumping views.

    By having a bit more control over animation you might have better luck finding out the issue or fixing it. For your specific case it might already be enough to do your animation using snapshots. Check out the following:

    @IBOutlet weak var label: UILabel!
    
    private func animateAsCustom(applyChanges: @escaping (() -> Void)) {
        guard let viewToMove = label else { return }
        guard let panel = viewToMove.superview else { return }
        guard let snapshotView = viewToMove.snapshotView(afterScreenUpdates: true) else { return }
        
        applyChanges()
        
        UIView.performWithoutAnimation {
            panel.addSubview(snapshotView)
            snapshotView.center = viewToMove.center
            viewToMove.transform = CGAffineTransform(translationX: 0.0, y: 50.0) // TODO: compute values for translation
            viewToMove.alpha = 0.0
        }
        UIView.animate(withDuration: 0.5, delay: 0.0, options: .curveEaseIn) {
            viewToMove.transform = .identity
            snapshotView.transform = CGAffineTransform(translationX: 0.0, y: -50.0)
            snapshotView.alpha = 0.0
            viewToMove.alpha = 1.0
        } completion: { _ in
            snapshotView.removeFromSuperview()
        }
    }
    
    @IBAction func didTap() {
        animateAsCustom {
            self.label.numberOfLines = 1
            self.label.text = "Can you see a flicker?"
            self.label.textColor = .red
            self.label.font = UIFont.systemFont(ofSize: 20)
        }
    }
    
    @IBAction func didTapRev() {
        animateAsCustom {
            self.label.numberOfLines = 0
            self.label.text = "Hello this\nis animation !!!"
            self.label.textColor = .black
            self.label.font = UIFont.systemFont(ofSize: 30)
        }
    }
    

    This will still not fix the issue if you press one of the buttons before previous animation did finish. To fix that one as well some extra effort may be needed. But for now this solution could be enough.