I'm desperately trying to morph a smallLabel into a bigLabel. By morphing, I mean transforming the following properties from one label to match the respective properties of the other label, with a smooth animation:
The desired effect should look similar to the animation that is applied to the navigation controller's title label when using large titles:

Now I'm aware of last year's WWDC session Advanced Animations with UIKit where they show how to do just that. However, this technique is pretty limited as it's basically just applying a transform to the label's frame and thus it only works if all properties other than the font size are identical.
The technique fails already when one label has a regular font weight and the other a bold weight – those properties do not change when applying a transform. Thus, I decided to dig a little deeper and use Core Animation for morphing.
First, I create a new text layer which I set up to be visually identical with the smallLabel:
/// Creates a text layer with its text and properties copied from the label.
func createTextLayer(from label: UILabel) -> CATextLayer {
let textLayer = CATextLayer()
textLayer.frame = label.frame
textLayer.string = label.text
textLayer.opacity = 0.3
textLayer.fontSize = label.font.pointSize
textLayer.foregroundColor = UIColor.red.cgColor
textLayer.backgroundColor = UIColor.cyan.cgColor
view.layer.addSublayer(textLayer)
return textLayer
}
Then, I create the necessary animations and add them to this layer:
func animate(from smallLabel: UILabel, to bigLabel: UILabel) {
let textLayer = createTextLayer(from: smallLabel)
view.layer.addSublayer(textLayer)
let group = CAAnimationGroup()
group.duration = 4
group.repeatCount = .infinity
// Animate font size
let fontSizeAnimation = CABasicAnimation(keyPath: "fontSize")
fontSizeAnimation.toValue = bigLabel.font.pointSize
// Animate font (weight)
let fontAnimation = CABasicAnimation(keyPath: "font")
fontAnimation.toValue = CGFont(bigLabel.font.fontName as CFString)
// Animate bounds
let boundsAnimation = CABasicAnimation(keyPath: "bounds")
boundsAnimation.toValue = bigLabel.bounds
// Animate position
let positionAnimation = CABasicAnimation(keyPath: "position")
positionAnimation.toValue = bigLabel.layer.position
group.animations = [
fontSizeAnimation,
boundsAnimation,
positionAnimation,
fontAnimation
]
textLayer.add(group, forKey: "group")
}
Here is what I get:

As you can see, it doesn't quite work as intended. There are two issues with this animation:
The font weight doesn't animate but switches abruptly in the middle of the animation process.
While the frame of the (cyan colored) text layer moves and increases in size as expected, the text itself somehow moves towards the lower-left corner of the layer and is cut off from the right side.
My questions are:
and
Probably something more simple than you think. Just snapshot the layers or views. The red text bleeds in the video of the apple transition so they are both just being blended together either with a snapshot or just a transform. I tend to snapshot views so as to not effect the real view underneath. Here is a UIView animation although the same thing could be done with CAAnimations.
import UIKit
class ViewController: UIViewController {
lazy var slider : UISlider = {
let sld = UISlider(frame: CGRect(x: 30, y: self.view.frame.height - 60, width: self.view.frame.width - 60, height: 20))
sld.addTarget(self, action: #selector(sliderChanged), for: .valueChanged)
sld.value = 0
sld.maximumValue = 1
sld.minimumValue = 0
sld.tintColor = UIColor.blue
return sld
}()
lazy var fakeNavBar : UIView = {
let vw = UIView(frame: CGRect(origin: CGPoint(x: 0, y: 20), size: CGSize(width: self.view.frame.width, height: 60)))
vw.autoresizingMask = [.flexibleWidth]
return vw
}()
lazy var label1 : UILabel = {
let lbl = UILabel(frame: CGRect(x: 10, y: 5, width: 10, height: 10))
lbl.text = "HELLO"
lbl.font = UIFont.systemFont(ofSize: 17, weight: .light)
lbl.textColor = .red
lbl.sizeToFit()
return lbl
}()
lazy var label2 : UILabel = {
let lbl = UILabel(frame: CGRect(x: 10, y: label1.frame.maxY, width: 10, height: 10))
lbl.text = "HELLO"
lbl.font = UIFont.systemFont(ofSize: 40, weight: .bold)
lbl.textColor = .black
lbl.sizeToFit()
return lbl
}()
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
self.view.addSubview(fakeNavBar)
self.fakeNavBar.addSubview(label1)
self.fakeNavBar.addSubview(label2)
self.view.addSubview(slider)
doAnimation()
}
func doAnimation(){
self.fakeNavBar.layer.speed = 0
let snap1 = label1.createImageView()
self.fakeNavBar.addSubview(snap1)
label1.isHidden = true
let snap2 = label2.createImageView()
self.fakeNavBar.addSubview(snap2)
label2.isHidden = true
let scaleForSnap1 = snap2.frame.height/snap1.frame.height
let scaleForSnap2 = snap1.frame.height/snap2.frame.height
let snap2Center = snap2.center
let snap1Center = snap1.center
snap2.transform = CGAffineTransform(scaleX: scaleForSnap2, y: scaleForSnap2)
snap2.alpha = 0
snap2.center = snap1Center
UIView.animateKeyframes(withDuration: 1.0, delay: 0, options: .calculationModeCubic, animations: {
UIView.addKeyframe(withRelativeStartTime: 0, relativeDuration: 0.5, animations: {
snap1.alpha = 0.2
})
UIView.addKeyframe(withRelativeStartTime: 0, relativeDuration: 0.5, animations: {
snap2.alpha = 0.2
})
UIView.addKeyframe(withRelativeStartTime: 0.5, relativeDuration: 0.5, animations: {
snap2.alpha = 1
})
UIView.addKeyframe(withRelativeStartTime: 0.5, relativeDuration: 0.1, animations: {
snap1.alpha = 0
})
UIView.addKeyframe(withRelativeStartTime: 0, relativeDuration: 1, animations: {
snap1.center = snap2Center
snap2.transform = .identity
snap2.center = snap2Center
snap1.transform = CGAffineTransform(scaleX: scaleForSnap1, y: scaleForSnap1)
})
}) { (finished) in
self.label2.isHidden = false
snap1.removeFromSuperview()
snap2.removeFromSuperview()
}
}
@objc func sliderChanged(){
if slider.value != 1.0{
fakeNavBar.layer.timeOffset = CFTimeInterval(slider.value)
}
}
}
extension UIImage {
convenience init(view: UIView) {
UIGraphicsBeginImageContext(view.frame.size)
view.layer.render(in:UIGraphicsGetCurrentContext()!)
let image = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
self.init(cgImage: image!.cgImage!)
}
}
extension UIView {
func createImageView() ->UIImageView{
let imgView = UIImageView(frame: self.frame)
imgView.image = UIImage(view: self)
return imgView
}
}
RESULT:

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With