CoreAnimation is pure love ❤️

I love animations. They make users happy, they can express more than just some static UI element.

Recently, a friend of mine, Rafael Ramos (@rakaramos), showed me a code that he wrote to create an animation that he saw in CreativeDash’s Dribbble. Check this out here.

I thought that it was very cool and decided to do the same because it is such a great opportunity to:

  • Practice Swift
  • Learn more about CoreAnimation
  • Create an open source UI component
  • Have fun! 😊

The challenge

This is the animation that I have picked:

https://dribbble.com/shots/1911260-Loading-with-Swift

At first, I was like: "Ok, this is just bezier stuff" but, writing a bezier for this balloon shape is just crazy sh*t.

So what? Here’s the silver bullet for this challenge: Paintcode App!

Paintcode App

With the help of Paintcode, I was able to visually create the balloon shape and it just creates the UIBezierPath for you! This is like magic, believe me.

With the UIBezierPath in my hands, I had like 50% of the challenge complete. The things that I had left were: create the balloon view, create the 3 dots and their animation.


The Balloon

At first, I tried to create 3 CAShapeLayers, one for each dot. Then I animated them separately, firing a method with some delays and NSTimer. It was ok, just working and looking kind of similar to the CreativeDash’s animation.

But then, in the office, on my way to the kitchen to grab a cup of coffee, I was passing by Rafael’s desk and he just threw something:

"You should think about to use the CAReplicatorLayer for that dots!"

It was another cool chance to explore something that I’ve never used before. 
I already read some articles about the CAReplicatorLayer (and I believe that one of them is posted right here on medium.com) and I didn’t realized that it should help a lot instead of those separated layers.

This is what MessageBalloon view looks like:

@IBDesignable
class MessageBalloon: UIView {
   //Some IBInspectable properties so anyone can change these values right in the Storyboard:
@IBInspectable var lineWidth:CGFloat = 5
@IBInspectable var color: UIColor = UIColor.clearColor()
@IBInspectable var lineColor:UIColor = UIColor.blackColor()
@IBInspectable var dotColor:UIColor = UIColor.blackColor() {
didSet { //This didSet will help us to change the dots color, because they are a separate view, so they will behave changing the color when updating this property.
dots.dotColor = dotColor
}
}
   //This is the shape of the balloon.
var bezierPath = UIBezierPath()
   override class func layerClass() -> AnyClass {
return CAShapeLayer.self
}

private func shapeLayer() -> CAShapeLayer {
return layer as! CAShapeLayer
}

//This is where the views will be updated whenever the view in the Storyboard changes.
override func layoutSubviews() {
super.layoutSubviews()

backgroundColor = UIColor.clearColor()

//The Balloon shape. (don't be scared! This was just copied from Paintcode and pasted right here:
bezierPath.moveToPoint(CGPointMake(127.63, 28.23))
bezierPath.addCurveToPoint(CGPointMake(127.63, 72.77), controlPoint1: CGPointMake(140.12, 40.53), controlPoint2: CGPointMake(140.12, 60.47))
bezierPath.addCurveToPoint(CGPointMake(87.79, 77.06), controlPoint1: CGPointMake(116.81, 83.42), controlPoint2: CGPointMake(100.17, 84.85))
bezierPath.addCurveToPoint(CGPointMake(74, 81), controlPoint1: CGPointMake(86.02, 77.56), controlPoint2: CGPointMake(74, 81))
bezierPath.addCurveToPoint(CGPointMake(78.78, 68.57), controlPoint1: CGPointMake(74, 81), controlPoint2: CGPointMake(77.82, 71.07))
bezierPath.addCurveToPoint(CGPointMake(73.17, 47.25), controlPoint1: CGPointMake(74.27, 62.24), controlPoint2: CGPointMake(72.4, 54.63))
bezierPath.addCurveToPoint(CGPointMake(82.37, 28.23), controlPoint1: CGPointMake(73.9, 40.3), controlPoint2: CGPointMake(76.97, 33.55))
bezierPath.addCurveToPoint(CGPointMake(127.63, 28.23), controlPoint1: CGPointMake(94.87, 15.92), controlPoint2: CGPointMake(115.13, 15.92))

//Balloon configuration (Applying the properties here)
shapeLayer().path = CGPath.rescaleForFrame(bezierPath.CGPath, frame: frame)
shapeLayer().strokeColor = lineColor.CGColor
shapeLayer().fillColor = color.CGColor
shapeLayer().lineJoin = kCALineJoinRound
shapeLayer().lineWidth = lineWidth

//Adding the dots
dots = Dots(frame: bounds)
dots.dotColor = dotColor

addSubview(dots)
}
}

Since the balloon itself does not have any animation, we are done with that. 
Please ignore the dots part, because I will be talking about them in the next chapter of this love story.
Let’s move to the dots and their animation.


The Dots

Properties

private var caLayer: CALayer = CALayer()

//The dots color
var
dotColor = UIColor.blackColor() {
didSet {
caLayer.backgroundColor = dotColor.CGColor
}
}
//The replicator layer
private var replicator: CAReplicatorLayer {
get {
return layer as! CAReplicatorLayer
}
}

Overrides and config

override class func layerClass() -> AnyClass {
return CAReplicatorLayer.self
}
override func layoutSubviews() {
super.layoutSubviews()
   //Replicator
replicator.backgroundColor = UIColor.clearColor().CGColor
replicator.instanceCount = 3 //Because we want 3 dots.
replicator.instanceTransform = CATransform3DMakeTranslation(dotSize() * 1.6, 0.0, 0.0)
replicator.instanceDelay = 0.1
   //Layer
caLayer = CALayer()
caLayer.bounds = CGRect(x: 0.0, y: 0.0, width: dotSize(), height: dotSize())
caLayer.position = CGPoint(x: center.x — (dotSize() * 1.6), y: center.y + (dotSize() * 0.2))
caLayer.cornerRadius = dotSize() / 2
caLayer.backgroundColor = dotColor.CGColor //the custom color
replicator.addSublayer(caLayer)

animationStart()
}

//This /10 is also to make the dots proportional, so the ball will have 1/10 size of the view
func
dotSize() -> CGFloat {
return self.frame.size.height / 10
}

Notice that there’s some "dotSize() * 1.6" around. The reason is because we want the balls size, spacing and position to be proportionally aligned according to the view size.

Animations

The factory

//Just to help use to invoke those animations properly.
enum
AnimationKeyPath:String {
case
scale = “transform.scale”,
yPosition = “position.y”,
opacity = “opacity”
}
//This will create a CASprintAnimation given a keyPath (That is the enum above), fromValue, toValue and the duration.
func createAnimation(keyPath:AnimationKeyPath, fromValue:CGFloat, toValue:CGFloat, duration:CFTimeInterval) -> CASpringAnimation {
   let animation = CASpringAnimation(keyPath: keyPath.rawValue)
animation.fromValue = fromValue
animation.toValue = toValue
animation.duration = duration
animation.removedOnCompletion = false
animation.fillMode = kCAFillModeForwards;
return animation
}

//This will create a CAAnimationGroup given a duration, a name (that will be used to switch between the 2 animations that we gonna have,  and the list of animations.
func animationGroup(duration:CFTimeInterval, name:String, animations:[CASpringAnimation]) -> CAAnimationGroup {

let animationGroup = CAAnimationGroup()
animationGroup.animations = animations
animationGroup.duration = duration
animationGroup.setValue(name, forKey: “animation”)
animationGroup.delegate = self
animationGroup.removedOnCompletion = false
animationGroup.fillMode = kCAFillModeForwards;
return animationGroup
}

The Up and down animations

When I was studying the way I was going to make the dots animation, I saw that I could separate the animation into 2 animations: Up and Down. And these animations will be a joint of another 3 small animations:

  • transform.scale (the dots grow up and down for each animation)
  • position.y (the dots will move up for each animation)
  • opacity (the dots will fade out and fade in for each animation)

What change between the up and down animations? Just parameters! That’s why I decided to create those factory functions.

Let’s take a deeper look into the up and down animations:

Up and Down Animations

//Animation Up
func animationStart() {
let move = createAnimation(.yPosition, fromValue:caLayer.position.y, toValue: caLayer.position.y — dotSize(), duration: 0.5)
let alpha = createAnimation(.opacity, fromValue: 1.0, toValue: 0.0, duration: 0.5)
let scale = createAnimation(.scale, fromValue: 1.0, toValue: 1.3, duration: 0.5)
let anim = animationGroup(0.5, name: “up”, animations: [move, alpha, scale])
caLayer.addAnimation(anim, forKey: nil)
}
//Animation Down
func animationEnd() {
let move = createAnimation(.yPosition, fromValue:caLayer.position.y + 5, toValue:caLayer.position.y, duration:0.2)
let alpha = createAnimation(.opacity, fromValue: 0.0, toValue: 1.0, duration: 0.5)
let scale = createAnimation(.scale, fromValue: 0.5, toValue: 1.0, duration: 0.3)
let anim = animationGroup(0.7, name: “down”, animations: [move, alpha, scale])
caLayer.addAnimation(anim, forKey: nil)
}

After implementing the up and down animations, the only thing that have left for the animations, is switching between them. And to do so, let’s implement the animationDidStop delegate:

// Animation Delegate
override func animationDidStop(anim: CAAnimation, finished flag: Bool) {
if (anim.valueForKey(“animation”) as! String == “up”) {
animationEnd()
} else if (anim.valueForKey(“animation”) as! String == “down”) {
animationStart()
}
}

The last part, is making sure that the view will scale proportionally according with it’s size, and for that I’m using a CGPath extension with a function called rescaleForFrame (credits to Rafael as well).

Here’s the full MessageBalloon.swift


Final Result

Quite similar, huh?

That’s it for today, folks. I really hope I have inspired you to learn more about CoreAnimation and it’s possibilities. Let’s make more beautiful and animated apps for our beloved users! They really deserve it.

If you want, you can check the MessageBalloon on Github as well.

Kind Regards,
Carlos.

One clap, two clap, three clap, forty?

By clapping more or less, you can signal to us which stories really stand out.