Tutorial: drawRect animation using POP

In this tutorial we will create Youtube play button animation using Facebook POP. The source code will be shared on Github, it also includes an alternative way of doing this animation using CoreAnimation.

For this tutorial we need CocoaPods. If you are not familiar with that, I would recommend to read this tutorial or at least CocoaPods Getting Started.


To begin with, we need to create a new Single View Application project (File -> New -> Project). After project is created, we need to add CocoaPods. Add this pod and install it:

pod 'pop'

POP still does not have Swift support, so we have to create bridging header which allows us to user Swift and Objective-C in the same project:

  1. Create .h file (File -> New -> File -> Source -> Header File) and name it BridgingHeader;
  2. Go to target build settings and search for Objective-C Bridging Header;
  3. Provide path to your bridging header. It should be ProjectName/BridgingHeader.h

Add this import to your BridgingHeader.h:

#import "POP.h"

Build the project to make sure that bridging header works properly and there are no warnings. It may fail because of enabled bitcode, as POP does not support it yet. To fix this error, you have to disable bitcode by going to your Target -> Build Settings -> Search for “bitcode:


For animation we need to create a new UIButton and call it PlayButton.

As you have seen, this button will animate between two states: Paused and Playing. We need to define them using enum. Copy and paste this code above class declaration:

enum PlayButtonState {
case Paused
case Playing
  var value: CGFloat {
return (self == .Paused) ? 1.0 : 0.0
}
}

The reason why I added var value: CGFloat will be described later.

Now I would like to go threw the theory of how button will animate between these two states.

POP allows us to create animatable property — and that is exactly what we are going to do. We will create CGFloat variable named animationValue and will animate it from 1.0 to 0.0 when button state is changed from Paused to Playing, and animate from 0.0 to 1.0 when button state is changed from Playing to Paused. Every time value has changed we will call setNeedsDisplay which will force our view to redraw. I think, now is a good time to try it!

Okay, lets declare some variables! Put this code at the beginning of class declaration:

// MARK: -
// MARK: Vars
private(set) var buttonState = PlayButtonState.Paused
private var animationValue: CGFloat = 1.0 {
didSet {
setNeedsDisplay()
}
}

Now we have two variables. First one is buttonState. This value can be accessed outside the class, but it can be set only inside the class. Second variable is animationValue. As you can see, we call setNeedsDisplay on didSet as it was explained above.

Next step is to create method which will be responsible for setting up animation or only updating animationValue when animation is not needed. The only reason why I added animated: Bool to this method is that if you will use this button in table view or collection view you will have to set state without animation when cell displayed.

// MARK: -
// MARK: Methods
func setButtonState(buttonState: PlayButtonState, animated: Bool) {
// 1
if self.buttonState == buttonState {
return
}
self.buttonState = buttonState
  // 2
if pop_animationForKey("animationValue") != nil {
pop_removeAnimationForKey("animationValue")
}
  // 3
let toValue: CGFloat = buttonState.value
  // 4
if animated {
let animation: POPBasicAnimation = POPBasicAnimation()
if let property = POPAnimatableProperty.propertyWithName("animationValue", initializer: { (prop: POPMutableAnimatableProperty!) -> Void in
prop.readBlock = { (object: AnyObject!, values: UnsafeMutablePointer<CGFloat>) -> Void in
if let button = object as? PlayButton {
values[0] = button.animationValue
}
}
prop.writeBlock = { (object: AnyObject!, values: UnsafePointer<CGFloat>) -> Void in
if let button = object as? PlayButton {
button.animationValue = values[0]
}
}
prop.threshold = 0.01
}) as? POPAnimatableProperty {
animation.property = property
}
animation.fromValue = NSNumber(float: Float(self.animationValue))
animation.toValue = NSNumber(float: Float(toValue))
animation.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseOut)
animation.duration = 0.25
pop_addAnimation(animation, forKey: "percentage")
} else {
animationValue = toValue
}
}
  1. We return if buttonState has not changed, and update buttonState if it has changed;
  2. We remove previous animation if it exists;
  3. We create immutable variable toValue and set value of the buttonState which is calculated in var value: CGFloat (Swift allows us to have very clean and nice implementation of vars in enumerations);
  4. If animated == true, then we initialise POPBasicAnimation with POPAnimatableProperty. Otherwise we only set animationValue.

Lets understand how we initialised POPBasicAnimation in our example. FromValue, toValue, timingFunction, duration — everything is simple and does not require explanation, right? All these parameters can be used in CABasicAnimation. The only complex part of POPBasicAnimation is POPAnimatableProperty. As we animate our own property, we have to initialise our own animation block which contains:

  1. readBlock — reads values from a property and stores in an array of floats (CGFloat values[]);
  2. writeBlock — writes values from array of floats into property;
  3. threshold —defines the smallest increment of the value.

More information can be found in here.

If we would like to animate built-in property it would be easier — we could just use

POPAnimatableProperty.propertyWithName(kPOPViewAlpha)

to animate view alpha. List of built-in properties can be found here.


Now we have everything we need to animate property and I would like to explain logic of animation, so we understand what to write in drawRect.

Assume, that we are going to animate from Paused to Playing (1 to 3). Our “triangle” button will split in two halves — trapeziums (2), and will animate them to rectangles. The easiest way to achieve this effect is to always have 2 geometrical figures consisting of four points:

Next step is to understand which values we need to calculate. I have prepared special image which describes all values very well:

  1. minWidth — this value never changes, it defines minimum width of left and right halves (I like when it is equal to 32% of full button width);
  2. aWidth — this value is calculated using animationValue. When animationValue = 1, this value will be zero. When animationValue = 0, this value will be equal to half of button width minus minimum width;
  3. width — equals to minimum width plus additional width (minWidth + aWidth);
  4. H1 — padding from top to the left half top right point and the right half top left point, AND padding from bottom to the left half bottom right point and the right half bottom left point. When animationValue = 0, this value will be equal height / 4. When animationValue = 1, this value will be equal 0;
  5. H2 — padding from top to the right half top right point AND padding from bottom to the right half bottom right point. When animationValue = 0, this value will be equal height / 2. When animation value = 1, this value will be equal 0.

Okay, I think it is enough theory and good time to start. Even if you did not understand something — please do not be scared, you will understand it once we override drawRect. Copy and paste this code in PlayButton:

// MARK: -
// MARK: Draw
override func drawRect(rect: CGRect) {
super.drawRect(rect)
  // 1
let height = rect.height
  let minWidth = rect.width * 0.32
let aWidth = (rect.width / 2.0 - minWidth) * animationValue
let width = minWidth + aWidth
  let h1 = height / 4.0 * animationValue
let h2 = height / 2.0 * animationValue
  // 2
let context = UIGraphicsGetCurrentContext()
  // 3
CGContextMoveToPoint(context, 0.0, 0.0)
CGContextAddLineToPoint(context, width, h1)
CGContextAddLineToPoint(context, width, height - h1)
CGContextAddLineToPoint(context, 0.0, height)
  CGContextMoveToPoint(context, rect.width - width, h1)
CGContextAddLineToPoint(context, rect.width, h2)
CGContextAddLineToPoint(context, rect.width, height - h2)
CGContextAddLineToPoint(context, rect.width - width, height - h1)
  // 4
CGContextSetFillColorWithColor(context, tintColor.CGColor)
CGContextFillPath(context)
}

This code is straightforward:

  1. We calculate all values as per image above. As I mentioned before, minWidth value is always the same, despite the fact that animationValue changes. I like when it is equal to 32 percent of the whole width. You can adjust this value as you want, but keep in mind, that it should be less or equal to half of width;
  2. As we are overriding drawRect, we should not to create a new context — we should get and use existing;
  3. Create CGPath of two trapezius, as it was described before;
  4. Set fill colour (I am using tintColor, feel free to change it).

The last step is to add PlayButton to our ViewController and test how it animates. Delete everything inside ViewController class and paste this:

// MARK: -
// MARK: Vars
private var playButton: PlayButton!
// MARK: -
override func viewDidLoad() {
super.viewDidLoad()
  playButton = PlayButton()
playButton.frame = CGRect(x: floor((view.bounds.width - 150.0) / 2.0), y: 50.0, width: 150.0, height: 150.0)
playButton.addTarget(self, action: Selector("playButtonPressed"), forControlEvents: .TouchUpInside)
view.addSubview(playButton)
}
// MARK: -
// MARK: Methods
func playButtonPressed() {
if playButton.buttonState == .Playing {
playButton.setButtonState(.Paused, animated: true)
} else {
playButton.setButtonState(.Playing, animated: true)
}
}

Now build your project and enjoy! This animation is unbreakable — you can tap button as quick as possible and it will always smoothly animate between two states without any visual breaks. Pretty good, isn’t it?


If you are excited about drawRect animations, please let me know, I will write second part of this tutorial with 2 more nice animations!

All source code from this tutorial PLUS CoreAnimation implementation can be found here.

Thank you for reading, I hope you enjoyed! Please let me know which animation would you use in your project — drawRect, or CA. Cannot wait to read your comments!

Show your support

Clapping shows how much you appreciated Danil Gontovnik’s story.