How We Built Pull To Make Soup Animation

Pull-to-refresh is a great place for creativity! But we’re building cool pull-to-refresh animations not only for the purpose of self-expression. These small components can actually help app publishers make their applications stand out. You probably saw our previous pull-to-refresh animations: Phoenix for Android and Pull-to-Refresh.Rentals for iOS can be a great feature for a real-estate app, and our Taurus Android animation is an execellent addition to any travel application.

Check our GitHub repository for more animations.

In this article we’re going to tell you how we built Pull To Make Soup animation. Check it out on Dribbble and GitHub. As all great ideas, the concept of this animation came about out of sudden.

“I wanted to make a unique pull-to-refresh animation but I couldn’t come up with a good idea for a long time,” said Sergey Ganushchak, our UI/UX designer. “I remember standing in the kitchen looking at the stove, and it suddenly came to me! I was thinking what if soup could prepare by itself? I started picturing vegetables falling into the saucepan, water pouring in, fire turning on. And then, I just opened After Effects and made it.”

Pull To Make Soup animation was developed by our iOS developer Anastasiya Gorban, who also wrote a reusable and customizable pull-to-refresh component for developers to use in their future projects.

Read also: How we built Guillotine Menu animation

Development story

by Anastasiya Gorban

At first I’m going to tell you how I implemented pull-to-refresh customizable component which you can absolutely use for developing your own pull-to-refresh animations, and then we’ll dwell on developing the Pull To Make Soup animation with all those cooking things included. You can check source code here.

There are two ways to create Pull-To-Refresh – easy and a more difficult one.

The easiest way to create Pull-To-Refresh

let refresher = PullToRefresh()

It will create a default pull-to-refresh with a simple view, and a single UIActivitiIndicatorView. To add refresher to your scroll view:

scrollView.addPullToRefresh(refresher, action: {
 // action to be performed (usually some loading from network)

After the action is complete and you want to hide the refresher:


You can also start refreshing programmatically:


But you probably won’t use this component, though. UITableViewController and UICollectionViewController already have a simple type of refresher like the one I created.

It’s much more interesting to develop your own pull-to-refresh control, isn’t it?

How to create a custom refresher?

To create a custom refresher you would need to initialize PullToRefresh class with two objects:

  • refreshView is a UIView object which will added to your scroll view.
  • animator is an object which will animate elements on refreshView depending on the state of PullToRefresh.
let awesomeRefrehser = PullToRefresh(refresherView: yourView, animator: yourAnimator)

Now let’s create a custom Pull-To-Refresh of our Pull To Make Soup animation.


1) Create a custom UIView with xib and add all images that you want to animate as subviews. Pin them with outlets:

class SoupView: UIView {
 private var pan: UIImageView!
 private var cover: UIImageView!
 private var potato: UIImageView!
 private var carrot: UIImageView!
 // and others


2) Create an Animator object that conforms RefreshViewAnimator protocol and can be initialized by your custom view.

class SoupAnimator: RefreshViewAnimator {
 private let refreshView: SoupView
 init(refreshView: SoupView) {
 self.refreshView = refreshView

 func animateState(state: State) {
 // animate refreshView according to state

3) According to RefreshViewAnimator protocol, your animator should implement animateState method. This method is called by PullToRefresh object every time its state is changed. There are four states:

enum State:Equatable {
 case Inital, Loading, Finished
 case Releasing(progress: double)
  1. Initial - refresher is ready to be pulled.
  2. Releasing - refresher is in the process of releasing (by a user or programmatically). This state contains a double value which represents releasing progress from 0 to 1.
  3. Loading - refresher is in the loading state.
  4. Finished - loading is finished.

Depending on the state that your animator gets from the PullToRefresh, it has to animate the elements needed in refreshView:

func animateState(state: State) {
 switch state {
 case .Inital: // do inital layout for elements
 case .Releasing(let progress): // animate elements according to progress
 case .Loading: // start loading animations
 case .Finished: // show some finished state if needed

Time to give you the actual cooking recipe!

How we animated every element in the Pull To Make Soup

For different elements in the animation I used different animation approaches: UIView based animation, UIImageView animation, and Core Animation.

Here is the code from the SoupAnimator:

func animateState(state: State) {
 switch state {
 case .Inital: initalLayout()
 case .Releasing(let progress): releasingAnimation(progress)
 case .Loading: startLoading()
 case .Finished: // nothing here

All animations which I’ll describe below perform either releasingAnimation(progress) or startLoading().

Read also: How we created Folding Tab Bar Animation

Taking control of the animation timing

I used native Core Animation to implement almost all animations in this pull-to-refresh component. This framework works directly with GPU without bothering CPU, so you don’t only get smooth animations, but high frame rates as well. There is no need to define the frame of each animation. All you have to do is configure a few animation parameters, such as the start and the end points, and tell Core Animation to start.

One more reason why I chose this framework is that it has a built-in mechanism for taking full control of the animation timing. After all, it’s a user who controls timing and speed in our Pull To Make Soup animation.

Now let’s see how to animate all this cooking magic.

Animating the saucepan

It’s the simplest one here. Everything the saucepan does is changing Y position. To achieve this behavior I used CAKeyFrameAnimation:

let animation = CAKeyframeAnimation(keyPath: “transform.translation.y")
animation.values = [-200, 0]
animation.keyTimes = [0, 0.5]
animation.duration = 0.3
animation.beginTime = 0

pan.layer.addAnimation(animation, forKey: nil)
pan.layer.speed = 0

I defined what to change exactly (keyPath: “transform.translation.y"), and set target data values([-200, 0]), as well as the time at which each value should be reached([0, 0.5]).

The animation object builds animation by interpolating from one value to the next over the given time periods. The keyTimes property specifies time markers at which to apply each keyframe value. These values should be anywhere from 0 to 1. The final keyTime value for this animation equals 0.5 because it should be finished earlier than others.

I put above the code to the initalLayout() method which relates to the Initial state of our pull-to-refresh, because all Releasing animations should be defined before a user starts pulling them.

The last line (pan.layer.speed = 0) and the code below do the trick of taking control of the animation timing. At first, speed = 0 pauses the animation. Then, in the method releasingAnimation(progress) I change timeOffset value for the layer according to the current progress:

func releasingAnimation(progress: CGFloat) {
 pan.layer.timeOffset = 0.3 * progress

Animating the vegetables

As you can see in the gif, vegetables in the animation are moving along their own non-linear paths. The best way to describe these path in iOS is by UIBezierPath object.

According to Apple, the UIBezierPath class lets you define a path consisting of straight and curved line segments. But how would you reproduce this path for every vegetable exactly how you see them in the gif?

Creating bézier paths from scratch by adding points and lines one by one would’ve taken a great deal of time! But we came up with another approach which I call “ask a designer.” Sergey provided these paths by exporting them in *.svg format without any problem :)

Here is what .svg for the carrot looks like:

Screen Shot 2015-05-12 at 22.35.46.png

Then, with the help of PocketSVG iOS library I was able to convert *.svg files to UIBezierPath objects:

let path = PocketSVG.pathFromSVGFileNamed(“carrot-path.svg")
let bezierPath = UIBezierPath(CGPath: path.takeUnretainedValue())

After I got bezierPaths, I had to place them in the proper positions. I needed the path to start somewhere behind the pan and finish inside the saucepan.

let translate = CGAffineTransformMakeTranslation(xOffset, xOffset)

Now we should create CAKeyframeAnimation with the bezier path:

let animation = CAKeyframeAnimation(keyPath: "position")
animation.path = bezierPath
animation.duration = 0.3
animation.beginTime = 0

And add to the vegetable view layer:

let carrot = refreshView.carrot
carrot.layer.addAnimation(animation, forKey: nil)
carrot.layer.speed = 0

All the code above belongs to the initalLayout() method. In the releasingAnimation(progress) I do the same thing I did with the saucepan – change the timeOffset according to the current progress:

carrot.layer.timeOffset = 0.3 * progress

Animating the pan cover

Releasing animation for the cover is the same as for the vegetables, i.e. by using a bezier path, so I won’t repeat it again. But in comparison to the vegetables which remain still during loading animation, the cover shakes under pressure, just like it does on your stove.

When I looked closer at the shaking animation effect I found that it contains two different animations – rotation and changing Y position. These two animations should run  simultaneously on the same layer so I decided to use CAAnimationGroup to achieve this behaviour.

Firstly, let's create a cover rotation animation:

let coverRotationAnimation = CAKeyframeAnimation(keyPath: “ transform.rotation.z")
coverRotationAnimation.values = [0.05, 0, -0.05, 0, 0.07, -0.03, 0]
coverRotationAnimation.keyTimes = [0, 0.2, 0.4, 0.6, 0.8, 0.9, 1]
coverRotationAnimation.duration = 0.3
coverRotationAnimation.beginTime = 0

Then, an animation for changing position:

let coverPositionAnimation = CAKeyframeAnimation(keyPath: “ transform.rotation.z")
coverPositionAnimation.values = [-2, 0, -2, 1, -3, 0]
coverPositionAnimation.keyTimes = [0, 0.3, 0.5, 0.7, 0.9, 1]
coverPositionAnimation.duration = 0.3
coverPositionAnimation.beginTime = 0

Place this to the group, and tell it to repeat infinitely:

let animationGroup = CAAnimationGroup()
animationGroup.duration = 1;
animationGroup.repeatCount = FLT_MAX;
animationGroup.animations = [coverRotationAnimation, coverPositionAnimation];

 Finally, add the animation to the layer:

cover.layer.addAnimation(animationGroup, forKey: nil)
cover.layer.speed = 1

The code above is placed in the startLoading() method which means the animation group starts running immediately, so I didn’t have to set "zero" speed for the cover's layer.

Animating the water and bubbles

The water animation is pretty simple. It’s just about scaling the view height from the bottom to the top. But the thing is, if we just do the scaling, the water scaling direction will be wrong. It’ll move towards the pan’s bottom. To change the scaling direction we should change the layer’s anchorPoint property:

water.layer.anchorPoint = CGPointMake(0.5, 1.0)

and then, apply transform to the view:

water.transform = CGAffineTransformMakeScale(1, 0);
UIView.animateWithDuration(1, animations: 
 water.transform = CGAffineTransformMakeScale(1, 1)

To repeat the bubble animations, I added a method that creates a circle, puts it on the water’s layer at a random X position, and animates this circle by scaling it and changing Y position.

func addBubble() {
 let radius: CGFloat = 1
 let x = CGFloat(arc4random_uniform(water.frame.size.width)))
 let circle = UIView(frame: CGRectMake(x, water.frame.size.height, 2*radius, 2*radius))
 circle.layer.cornerRadius = radius
 circle.layer.borderWidth = 1
 circle.layer.masksToBounds = true
 circle.layer.borderColor = UIColor.whiteColor().CGColor
 UIView.animateWithDuration(1.3, animations: {
 let radius:CGFloat = 4
 circle.layer.frame = CGRectMake(x, -20, 2*radius, 2*radius)
 circle.layer.cornerRadius = radius
 }) { _ in

The method is invoked by the timer with a small interval:

bubbleTimer = NSTimer.scheduledTimerWithTimeInterval(0.12, target: self, selector: "addBubble", userInfo: nil, repeats: true)

Animating the flames

Using key frame animations to reproduce the flames animation was pretty hard, so I decided to use UIImageView object for animating a series of images. I resorted to “ask a designer” approach again, and asked Sergey to give me two sequences for fire: when it starts burning and when it’s burning.

let startBurningFlames: [UIImage] = // create images
flame.animationImages = startBurningFlames
flame.animationDuration = 0.7
let delayTime = dispatch_time(DISPATCH_TIME_NOW, Int64(0.7 * Double(NSEC_PER_SEC)))
dispatch_after(delayTime, dispatch_get_main_queue()) {
 let burningFlames: [UIImage] = // create images
 flame.animationImages = burningFlames
 flame.animationDuration = 2
 flame.animationRepeatCount = 0 // repeat infinitely

That’s it!

Now we have a magic soup in the saucepan that cooks itself and a customizable pull-to-refresh component. I hope it encourages you to create your own super cool animations to make your apps look gorgeous.

You can find all the code and design here:


4.4/ 5.0
Article rating
Remember those Facebook reactions? Well, we aren't Facebook but we love reactions too. They can give us valuable insights on how to improve what we're doing. Would you tell us how you feel about this article?
Excited to create something outstanding?

We share the same interests.

Contact us

We use cookies to personalize our service and to improve your experience on the website and its subdomains. We also use this information for analytics.