Creating Custom Animated Buttons

Login to Access Code

Inspired by a dribbble shot, we are going to create a custom animated UIButton. I hope that by the end of this tutorial you’ll be able to, at least, understand some of the important steps when it comes to coding animations.

Understanding the animation

First things first. Before we jump into XCode and play with transforms, CALayer subclasses and so on, it’s very important to understand what is actually occuring with the animation mockup that we are going to build. Try watch the animation for a while, and consider the instructions you would have to provide in order to simply describe what is occurring. EZGIF offers some very handy tools for gifs like changing the speed and watching in slow-motion, which should help:

image

Let’s start by breaking apart the animation layers and naming them for easier reference:

image

1: Star 2: Fill Circle 3: Ring

Okay, so now that we have identified the layers, we can describe what happens to each layer in sort of a frame-by-frame breakdown. Here is a simple description of the animation steps:

  1. Star and Ring grows at almost the same pace.
  2. Star and Ring stop growing, then starts shrinking to Star’s center at a faster pace than step 1.
  3. Fill Circle grows from Star’s center.
  4. Star grows to it’s initial size, and the filling color is changed.
  5. Fill Circle grows until reach the size of step 2 and shrink back to the initial size.

Creating paths

Every path in iOS is represented by CGPaths and creating them in code can be a really painful and time consuming task. Luckily there is a tool called PaintCode which makes it real easy. All you have to do is create your shapes on any vector based software like Adobe Illustrator or Sketch, export it as SVG, import into PaintCode, and then PaintCode will provide you with both Objective-C and Swift code for that shape.

The code below represents the Star:

var star = UIBezierPath()
star.moveToPoint(CGPointMake(112.79, 119))
star.addCurveToPoint(CGPointMake(107.75, 122.6), controlPoint1: CGPointMake(113.41, 122.8), controlPoint2: CGPointMake(111.14, 124.42))
star.addLineToPoint(CGPointMake(96.53, 116.58))
star.addCurveToPoint(CGPointMake(84.14, 116.47), controlPoint1: CGPointMake(93.14, 114.76), controlPoint2: CGPointMake(87.56, 114.71))
star.addLineToPoint(CGPointMake(72.82, 122.3))
star.addCurveToPoint(CGPointMake(67.84, 118.62), controlPoint1: CGPointMake(69.4, 124.06), controlPoint2: CGPointMake(67.15, 122.41))
star.addLineToPoint(CGPointMake(70.1, 106.09))
star.addCurveToPoint(CGPointMake(66.37, 94.27), controlPoint1: CGPointMake(70.78, 102.3), controlPoint2: CGPointMake(69.1, 96.98))
star.addLineToPoint(CGPointMake(57.33, 85.31))
star.addCurveToPoint(CGPointMake(59.29, 79.43), controlPoint1: CGPointMake(54.6, 82.6), controlPoint2: CGPointMake(55.48, 79.95))
star.addLineToPoint(CGPointMake(71.91, 77.71))
star.addCurveToPoint(CGPointMake(81.99, 70.51), controlPoint1: CGPointMake(75.72, 77.19), controlPoint2: CGPointMake(80.26, 73.95))
star.addLineToPoint(CGPointMake(87.72, 59.14))
star.addCurveToPoint(CGPointMake(93.92, 59.2), controlPoint1: CGPointMake(89.46, 55.71), controlPoint2: CGPointMake(92.25, 55.73))
star.addLineToPoint(CGPointMake(99.46, 70.66))
star.addCurveToPoint(CGPointMake(109.42, 78.03), controlPoint1: CGPointMake(101.13, 74.13), controlPoint2: CGPointMake(105.62, 77.44))
star.addLineToPoint(CGPointMake(122, 79.96))
star.addCurveToPoint(CGPointMake(123.87, 85.87), controlPoint1: CGPointMake(125.81, 80.55), controlPoint2: CGPointMake(126.64, 83.21))
star.addLineToPoint(CGPointMake(114.67, 94.68))
star.addCurveToPoint(CGPointMake(110.75, 106.43), controlPoint1: CGPointMake(111.89, 97.34), controlPoint2: CGPointMake(110.13, 102.63))
star.addLineToPoint(CGPointMake(112.79, 119))

We can create simple shapes in iOS by hand, such as the circle layers:

let circle = UIBezierPath(ovalInRect: inFrame)

Now we have everything we need to create our custom UIButton. It’s time to assemble!

Joining the pieces

Xcode 6 brings a new technology called live views which make custom layout code much easier to work with, and provides immediate visual feedback without building and running. So let’s use live view! The implementation is fairly simple to use live view with your class:

  1. Place the keyword @IBDesignable above your class declaration
  2. Place the keyword @IBInspectable above a variable which you want to change on Storyboard Inspector.
  3. Override layoutSubviews(). This is where you’re going add your subviews or sublayer.

For our Star button, we are going to create a UIButton subclass and follow the above steps:

@IBDesignable
class StarButton: UIButton
{
  private var starShape: CAShapeLayer!
  private var outerRingShape: CAShapeLayer!
  private var fillRingShape: CAShapeLayer!

  @IBInspectable
  var lineWidth: CGFloat = 1 {
    didSet {
      updateLayerProperties()
    }
  }

  @IBInspectable
  var favoriteColor: UIColor = UIColor(hex:"eecd34") {
    didSet {
      updateLayerProperties()
    }
  }

  @IBInspectable
  var notFavoriteColor: UIColor = UIColor(hex:"9e9b9b") {
    didSet {
      updateLayerProperties()
    }
  }

  @IBInspectable
  var starFavoriteColor: UIColor = UIColor(hex:"9e9b9b") {
    didSet {
      updateLayerProperties()
    }
  }

  var isFavorite: Bool = false {
    didSet {
      return self.isFavorite ? favorite() : notFavorite()
    }
  }

  private func updateLayerProperties()
  {
    if fillRingShape != nil
    {
      fillRingShape.fillColor = favoriteColor.CGColor
    }

    if outerRingShape != nil
    {
      outerRingShape.lineWidth = lineWidth
      outerRingShape.strokeColor = notFavoriteColor.CGColor
    }

    if starShape != nil
    {
      starShape.fillColor = isFavorite ? starFavoriteColor.CGColor : notFavoriteColor.CGColor
    }
  }

  override func layoutSubviews()
  {
    super.layoutSubviews()
    updateLayerProperties()
  }
}

Jump to storyboard, create a UIButton, set it to Custom, remove the default text on it, and set it to our StarButton class.

image

Set the @Inspectables on Inspector.

image

And voilá!

image

The animation

Creating animations are always a bit of trial and error. Of course, if you have an animation background, you will likely not have to build as frequently. For the sake of time, I will jump straight to the code. To create the animation, I have broke it down into five key parts with comments noting the more substantial components involved. The main properties we work with are CATransform3D for the transformation of size, opacity for alpha, and fillColor for color. Let’s assume that we are using this button as a way for the user to mark things as favorites, and name our functions accordingly.

private func favorite()
{
  // 1. Star grows
  var starGoUp = CATransform3DIdentity
  starGoUp = CATransform3DScale(starGoUp, 1.5, 1.5, 1.5)

  // 2. Star stop growing and starts shrinking
  var starGoDown = CATransform3DIdentity
  starGoDown = CATransform3DScale(starGoDown, 0.01, 0.01, 0.01)

  // Configure a keyframe animation with both transforms (grow and shrink)
  let starKeyFrames = CAKeyframeAnimation(keyPath: "transform")
  starKeyFrames.values = [
    NSValue(CATransform3D:CATransform3DIdentity),
    NSValue(CATransform3D:starGoUp),
    NSValue(CATransform3D:starGoDown)
  ]
  starKeyFrames.keyTimes = [0.0,0.4,0.6]
  starKeyFrames.duration = 0.4
  starKeyFrames.beginTime = CACurrentMediaTime() + 0.05
  starKeyFrames.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseIn)

  // This is VERY important when you're working with relative time, remove and odd things will happen
  starKeyFrames.fillMode =  kCAFillModeBackwards
  starKeyFrames.setValue(favoriteKey, forKey: starKey)

  // Let the notification tell us when it's over
  starKeyFrames.delegate = self
  starShape.addAnimation(starKeyFrames, forKey: favoriteKey)
  starShape.transform = starGoDown

  // 1. Ring grows
  var grayGoUp = CATransform3DIdentity
  grayGoUp = CATransform3DScale(grayGoUp, 1.5, 1.5, 1.5)

  // 2. Ring stop growing and starts shrinking
  var grayGoDown = CATransform3DIdentity
  grayGoDown = CATransform3DScale(grayGoDown, 0.01, 0.01, 0.01)

  let outerCircleAnimation = CAKeyframeAnimation(keyPath: "transform")
  outerCircleAnimation.values = [
    NSValue(CATransform3D:CATransform3DIdentity),
    NSValue(CATransform3D:grayGoUp),
    NSValue(CATransform3D:grayGoDown)
  ]
  outerCircleAnimation.keyTimes = [0.0,0.4,0.6]
  outerCircleAnimation.duration = 0.4
  outerCircleAnimation.beginTime = CACurrentMediaTime() + 0.01
  outerCircleAnimation.fillMode =  kCAFillModeBackwards
  outerCircleAnimation.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseIn)

  outerRingShape.addAnimation(outerCircleAnimation, forKey: "Gray circle Animation")
  outerRingShape.transform = grayGoDown

  // 3. Fill Circle grows from Star's center.
  var favoriteFillGrow = CATransform3DIdentity
  favoriteFillGrow = CATransform3DScale(favoriteFillGrow, 1.5, 1.5, 1.5)

  // 5. Fill Circle grows until reach the size of step 2 and shrink back to the initial size.
  let fillCircleAnimation = CAKeyframeAnimation(keyPath: "transform")

  fillCircleAnimation.values = [
    NSValue(CATransform3D:fillRingShape.transform),
    NSValue(CATransform3D:favoriteFillGrow),
    NSValue(CATransform3D:CATransform3DIdentity)
  ]
  fillCircleAnimation.keyTimes = [0.0,0.4,0.6]
  fillCircleAnimation.duration = 0.4
  fillCircleAnimation.beginTime = CACurrentMediaTime() + 0.22
  fillCircleAnimation.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseIn)
  fillCircleAnimation.fillMode =  kCAFillModeBackwards

  let favoriteFillOpacity = CABasicAnimation(keyPath: "opacity")
  favoriteFillOpacity.toValue = 1
  favoriteFillOpacity.duration = 1
  favoriteFillOpacity.beginTime = CACurrentMediaTime()
  favoriteFillOpacity.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseIn)
  favoriteFillOpacity.fillMode =  kCAFillModeBackwards

  fillRingShape.addAnimation(favoriteFillOpacity, forKey: "Show fill circle")
  fillRingShape.addAnimation(fillCircleAnimation, forKey: "fill circle Animation")
  fillRingShape.transform = CATransform3DIdentity
}

The second part of the animation is triggered when the first is over:

private func endFavorite()
{
  // just a helper to run this piece of code with default actions disabled
  executeWithoutActions {
    self.starShape.fillColor = self.starFavoriteColor.CGColor
    self.starShape.opacity = 1
    self.fillRingShape.opacity = 1
    self.outerRingShape.transform = CATransform3DIdentity
    self.outerRingShape.opacity = 0
  }

  // 4. Star grows to it's initial size, and the filling color is changed.
  let starAnimations = CAAnimationGroup()
  var starGoUp = CATransform3DIdentity
  starGoUp = CATransform3DScale(starGoUp, 2, 2, 2)

  let starKeyFrames = CAKeyframeAnimation(keyPath: "transform")
  starKeyFrames.values = [
    NSValue(CATransform3D: starShape.transform),
    NSValue(CATransform3D:starGoUp),
    NSValue(CATransform3D:CATransform3DIdentity)
  ]
  starKeyFrames.keyTimes = [0.0,0.4,0.6]
  starKeyFrames.duration = 0.2
  starKeyFrames.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionLinear)

  starShape.addAnimation(starKeyFrames, forKey: nil)
  starShape.transform = CATransform3DIdentity
}

For the “no star for you” animation, it has no secret:

private func notFavorite()
{
  let starFillColor = CABasicAnimation(keyPath: "fillColor")
  starFillColor.toValue = notFavoriteColor.CGColor
  starFillColor.duration = 0.3

  let starOpacity = CABasicAnimation(keyPath: "opacity")
  starOpacity.toValue = 0.5
  starOpacity.duration = 0.3

  let starGroup = CAAnimationGroup()
  starGroup.animations = [starFillColor, starOpacity]

  starShape.addAnimation(starGroup, forKey: nil)
  starShape.fillColor = notFavoriteColor.CGColor
  starShape.opacity = 0.5

  let fillCircle = CABasicAnimation(keyPath: "opacity")
  fillCircle.toValue = 0
  fillCircle.duration = 0.3
  fillCircle.setValue(notFavoriteKey, forKey: starKey)
  fillCircle.delegate = self

  fillRingShape.addAnimation(fillCircle, forKey: nil)
  fillRingShape.opacity = 0

  let outerCircle = CABasicAnimation(keyPath: "opacity")
  outerCircle.toValue = 0.5
  outerCircle.duration = 0.3

  outerRingShape.addAnimation(outerCircle, forKey: nil)
  outerRingShape.opacity = 0.5
}

A giveaway

Since Swift is the real star in the iOS community, let’s celebrate! Change the star path with the following CGPath:

var swiftPath = UIBezierPath()
swiftPath.moveToPoint(CGPointMake(376.2, 283.2))
swiftPath.addCurveToPoint(CGPointMake(349.8, 238.4), controlPoint1: CGPointMake(367.4, 258.4), controlPoint2: CGPointMake(349.8, 238.4))
swiftPath.addCurveToPoint(CGPointMake(236.5, 0), controlPoint1: CGPointMake(349.8, 238.4), controlPoint2: CGPointMake(399.7, 105.6))
swiftPath.addCurveToPoint(CGPointMake(269, 180.8), controlPoint1: CGPointMake(303.7, 101.6), controlPoint2: CGPointMake(269, 180.8))
swiftPath.addCurveToPoint(CGPointMake(181.29, 117.07), controlPoint1: CGPointMake(269, 180.8), controlPoint2: CGPointMake(211.4, 140.8))
swiftPath.addCurveToPoint(CGPointMake(85, 33.6), controlPoint1: CGPointMake(151.18, 93.35), controlPoint2: CGPointMake(85, 33.6))
swiftPath.addCurveToPoint(CGPointMake(145, 117.07), controlPoint1: CGPointMake(85, 33.6), controlPoint2: CGPointMake(128.15, 96.31))
swiftPath.addCurveToPoint(CGPointMake(185.78, 163.66), controlPoint1: CGPointMake(161.85, 137.84), controlPoint2: CGPointMake(185.78, 163.66))
swiftPath.addCurveToPoint(CGPointMake(136.36, 129.42), controlPoint1: CGPointMake(185.78, 163.66), controlPoint2: CGPointMake(161.07, 147.39))
swiftPath.addCurveToPoint(CGPointMake(34.6, 50.4), controlPoint1: CGPointMake(111.65, 111.46), controlPoint2: CGPointMake(34.6, 50.4))
swiftPath.addCurveToPoint(CGPointMake(133.8, 169.2), controlPoint1: CGPointMake(34.6, 50.4), controlPoint2: CGPointMake(82.69, 119.24))
swiftPath.addCurveToPoint(CGPointMake(214.6, 244), controlPoint1: CGPointMake(184.91, 219.16), controlPoint2: CGPointMake(214.6, 244))
swiftPath.addCurveToPoint(CGPointMake(129.8, 264.8), controlPoint1: CGPointMake(214.6, 244), controlPoint2: CGPointMake(196.2, 263.2))
swiftPath.addCurveToPoint(CGPointMake(0, 221), controlPoint1: CGPointMake(63.4, 266.4), controlPoint2: CGPointMake(0, 221))
swiftPath.addCurveToPoint(CGPointMake(206.6, 339.2), controlPoint1: CGPointMake(0, 221), controlPoint2: CGPointMake(62.5, 339.2))
swiftPath.addCurveToPoint(CGPointMake(325, 304.8), controlPoint1: CGPointMake(270.6, 339.2), controlPoint2: CGPointMake(288.93, 304.8))
swiftPath.addCurveToPoint(CGPointMake(383.3, 339.2), controlPoint1: CGPointMake(361.07, 304.8), controlPoint2: CGPointMake(381.7, 340))
swiftPath.addCurveToPoint(CGPointMake(376.2, 283.2), controlPoint1: CGPointMake(384.9, 338.4), controlPoint2: CGPointMake(385, 308))
return swiftPath.CGPath

And there you go! Yay! Swift!!!!

image

That’s it! I hope that this helped you understand a bit more about the thought process involved with creating animations. For the full implementation, go to my Github. I will improve the code as well as add some new shapes, so keep your eye on it! If you enjoyed the post, please consider sharing and starring the git repo!