How UIScrollView works

Ilya Lobanov
24 min readJun 22, 2020

--

Yandex.Metro on both iOS and Android uses the shared MetroKit library, written in C++. In particular, MetroKit has a SchemeView for displaying the metro map. And we were faced with the task of implementing a scroll for this scheme. We chose UIScrollView as a reference since we found its behavior the most natural and appropriate.

We divided the development process into three mechanics, three stages of implementing the UIScrollView behavior. The first of them is deceleration:

Deceleration

The second one is spring animation for implementing bounces on bounds:

Spring Animation

The third mechanic is the so-called rubber band effect for implementing scroll resistance at bounds:

Rubber Band Effect

I’m going to tell about each of these mechanics in detail, including what formulas are used for this and examples of when you can apply these formulas.

Test Example

Let’s look at the test SimpleScrollView, which we will use as an example to consider all the mechanics, all the formulas, and which we will gradually improve.

Similar to UIScrollView, it will have contentView, contentSize, and contentOffset:

To implement gestures, we will use UIPanGestureRecognizer and the handlePanRecognizer function

SimpleScrollView will have one of two states:

  • .default when nothing happens;
  • .dragging when we scroll.

The implementation of handlePanRecognizer will look like this:

  • when the UIPanGestureRecognizer enters .began state, we set the .dragging state.
  • when it enters the .changed state, we calculate translation and change the contentOffset of our scroll view accordingly. At the same time, we call the clampOffset function so that we don’t go beyond the content bounds.
  • when in the .ended state, we just return SimpleScrollView to the .default state.

Let’s set the contentOffsetBounds auxiliary property that defines the contentOffset bounds using the current contentSize. As well as the clampOffset function, which restricts contentOffset using these bounds.

And now we have a simple implementation of SimpleScrollView ready.

SimpleScrollView

Scroll is already working somehow. We don’t have animation yet, we don’t have the movement inertia. We will add all this gradually over the course of the presentation, thus improving our SimpleScrollView

Deceleration

Let’s start with deceleration.

There is no information in the SDK about how deceleration is implemented. UIScrollView has a DecelerationRate, which can be either .normal, or .fast, and you can use it to adjust the velocity of scroll deceleration. The documentation states that DecelerationRate determines the rate of scroll deceleration.

UIScrollView | https://developer.apple.com/documentation/uikit/uiscrollview

The formula for finding the end position of the scroll — the point where the scroll stops after you take your finger away — was demonstrated at the presentation of Designing Fluid Interfaces at WWDC.

This formula cannot be used to implement deceleration, but we can use it as a reference to test our further calculations. The function takes the initial velocity of the gesture and the deceleration rate and returns the end point at which the scroll would have stopped after we took the finger away.

Velocity

Let’s try to guess how deceleration can work and what DecelerationRate can mean overall. The documentation says that this is

A floating-point value that determines the rate of deceleration after the user lifts their finger.

We can assume that this rate indicates how much the scroll velocity will change after one millisecond (all UIScrollView values are expressed in milliseconds, unlike UIPanGestureRecognizer).

If at the moment of taking the finger away we had the velocity v₀ and we chose DecelerationRate.fast, then

  • after 1 millisecond velocity will be 0.99 of v₀;
  • after 2 milliseconds velocity will be 0.99² of v₀;
  • after k seconds, velocity will be 0. 99¹⁰⁰⁰k of v₀.

As a result, we got a formula for the velocity of decelerating movement:

Equation of motion

The velocity formula cannot be used to implement deceleration. We need to find the equation of motion: the dependence of the coordinate on time x(t). And the velocity formula will help us to find the equation of motion, we just need to integrate it:

Then substitute our velocity formula for v(x), integrate it and we get:

Endpoint equation

Now we can find the formula for the scroll endpoint, compare it with Apple’s formula, and test our reasoning. To do this, we need to direct time t to infinity. Since we have d less than one, and d¹⁰⁰⁰t converges to zero, we get:

Now let’s compare the found formula with Apple’s formula. Let’s write it out in the same notation:

And we see that the formulas are slightly different in the right-hand parts:

Ours and Apple’s

However, if we look at how the natural logarithm is decomposed into a Taylor series in the neighborhood of 1, we will see that the Apple formula is actually an approximation of our formula:

Natural Logarithm | https://en.wikipedia.org/wiki/Natural_logarithm#Series

If we plot the graphs of these functions, we will see that when approaching one, they almost coincide:

Let me remind you that the standard DecelerationRate values are very close to 1, which means that this optimization by Apple is quite correct.

Deceleration time

Now we are only left to find the deceleration time in order to create animations. To find the end point, we directed time to infinity. But we can’t use infinite time for animation.

If we plot the equation of motion, we can see that the function is infinitely close to the end point X. But at the same time, starting from a certain point in time, the function gets so close to the end point X that the movement becomes imperceptible.

Therefore, we can reformulate our problem as follows: we find a time T after which the function is close enough to the end point X (by some small distance ε). In practice, half a pixel may be taken as an example.

Let’s find a T at which the distance to the end point is equal to ε:

Substitute our formulas for x and X and get the formula for the time of decelerating motion:

And now we have all the information we need to implement deceleration on our own. These are the formulas we used for the metro scheme.

Now let’s improve our SimpleScrollView by adding deceleration to it.

Deceleration implementation

To begin with, let’s describe the DecelerationTimingParameters structure, which will contain all the necessary information to create an animation when you take your finger away:

  • initialValue is the initial contentOffset — the point where we took our finger away;
  • initialVelocity is the velocity of the gesture;
  • decelerationRate is the deceleration rate;
  • threshold is the threshold for finding the deceleration time.

Using our formulas, we will find the point where the scroll stops:

Deceleration duration:

And the equation of motion:

We will use TimerAnimation, which will call the passed animation callback 60 times per second when the screen is updated (or 120 times per second on the iPad Pro), as the animation:

We will use the animations block to use the current time to call the equation of motion and change the contentOffset accordingly. The TimerAnimation implementation can be found in the repository.

And now we will improve the gesture processing function:

Deceleration should start when the finger is taken away. Therefore, when the .ended state comes, we will call the startDeceleration function, passing the velocity of the gesture to it:

The implementation of startDeceleration will be as follows:

  • choose DecelerationRate.normal and the threshold of half a pixel;
  • initialize DecelerationTimingParameters;
  • run the animation, passing the animation time there, and we will call the equation of motion and update the contentOffset in the animations block, using the current time.

Here’s what we got:

Deceleration

This is all about deceleration, and now let’s talk about the spring animation.

Spring Animation

Let me remind you that we used the spring animation to implement bounce on bounds.

There is much more information about the spring animation, as opposed to deceleration. It is based on the damped oscillations of the spring. Therefore, the concepts are the same everywhere: in the iOS SDK, in the Android SDK, and in articles that describe the behavior of the spring.

Most often the spring is parameterized by:

  • mass (m)
  • stiffness (k)
  • damping (d)

The equations of motion of the spring looks like this. It also depends on the mass, stiffness, and damping:

Sometimes, instead of damping, you can find a damping ratio (ζ). They are linked by the following formula:

Damping Ratio

The most interesting parameter is the damping ratio. You can use it to determine what the animation will look like.

Damping Ratio: 0.1, 0.5 and 1.0

The closer the damping ratio is to 0, the more pronounced the oscillations are. And the closer they are to 1, the weaker they are. When the ratio is equal to 1, there are no jumps at all, the amplitude simply fades.

Depending on the ratio, the damped vibrations are divided into three types:

  • 0 < ζ < 1underdamped. In this case, there are jumps near the rest state. The closer the ratio is to zero, the more pronounced the jumps are.
  • ζ = 1 — critically damped. In this case, there are no jumps at all. A short-term increase in the amplitude is possible, but the oscillations fade exponentially over time.
  • ζ > 1 — overdamped. In this case, the oscillations simply fade exponentially. This case is rarely used, so we won’t consider it.

Equation of motion

As we already know, the equation of motion in general form looks as follows:

In practice, it is difficult to use it, so we need to find a solution to this equation in the form of x(t). We also need to find oscillation time in order to create animations. The solution to this equation is different for different damping ratios, so we will consider each of the cases separately.

Underdamped (0 < ζ < 1)

In the case when we have a ratio less than one (underdamped), the solution of the equation looks like this:

  • the coefficients C₁ and C₂ are found using initial conditions: the initial position of the point is x₀, and the initial velocity is v₀:

The sine and cosine on the left side tells us that the oscillation has a period, and the exponent — that the oscillations exponentially fade.

Visually, it looks like this: the amplitude decreases exponentially over time with some frequency due to the sine and cosine:

Critically damped (ζ = 1)

Now let’s consider the critically damped case. The equation of motion looks like this:

  • β — the same auxiliary parameter;
  • the coefficients C₁ and C₂ differ from the previous case, but they are also found using the initial conditions:

The graph will be as follows:

There are no longer any jumps, the amplitude just fades exponentially.

Oscillation time

Now we need to find oscillation time. Here the oscillation time is infinite, as in the case of deceleration, but we cannot use infinite time. But you can see that starting at some point, the oscillations are so small that they will be impossible to see:

So we reformulate the problem again: we find a time T after which the amplitude is less than some small value of ε (for example, half a pixel).

And using the same reasoning as in the case of deceleration, we first find the time for the underdamped system (0 < ζ < 1):

And we find the time for the critically damped system in the same way (ζ = 1):

As a result, we found formulas for implementing spring animation.

But you might have a question: why do I need to know in detail how spring animation works if the iOS SDK already has an implementation of it?

It’s clear that we couldn’t use the iOS SDK in Metro, since the MetroKit library is multi-platform, so we had to figure out how animation works. But it isn’t clear why this is necessary for SimpleScrollView, and it isn’t clear why iOS developers need it.

Spring Animation in the iOS SDK

The iOS SDK does have several ways to create a spring animation, and the easiest one is UIView.animate.

UIView.animate

UIView.animate is parameterized by the dampingRatio and initialSpringVelocity. But the peculiarity of this function is that we also need to pass the damping duration. But we can’t calculate it, because we don’t know the other parameters of the spring: we don’t know the mass or stiffness, and we don’t know the initial displacement of the spring.

This function solves a slightly different problem: we set the behavior of the spring using the damping ratio and the desired animation duration, and everything else will be automatically calculated in the implementation of the function.

Therefore, the UIView.animate function can be used for simple animations with a specific behavior and a specific duration without reference to coordinates. But this function won’t work for scroll view.

CASpringAnimation

Another way is CASpringAnimation:

Although CASpringAnimation is necessary for animating CALayer properties , and it would be at least inconvenient to use it, let’s also talk about it. CASpringAnimation is already parameterized by mass, stiffness, and damping, but not by the damping ratio. As we said earlier, it is the damping ratio that most determines the behavior of the spring. If we don’t want oscillations, then select 1, if we want strong oscillations, then select the values close to 0. But there is no such parameter here.

But after we’ve learned the formulas, we can extend the CASpringAnimation class and add a constructor that takes the damping ratio:

Also here you need to set the damping duration, as in the case of UIView.animate, but unlike UIView.animate, there is an auxiliary field settlingDuration, which would return the estimated damping duration based on the CASpringAnimation settings:

The problem here is that settlingDuration doesn’t take into account the spring offset in any way, it doesn’t take into account fromValue and toValue in any way. Whatever you set in fromValue and toValue, settlingDuration is always the same. This is done for versatility, because fromValue and toValue can be anything: it can be coordinates or color — and here it’s not clear how to calculate the offset of the spring.

And what actually happens here is the following. You probably know that when calling UIView.animate, you can pass an animation curve as a parameter: for example, .linear, .easeIn, .easeOut, or .easeInOut.

And this curve will indicate how the animation progress will change over time from 0 to 1.

Advanced Animations with UIKit

And the same goes for the spring animation in the iOS SDK. The spring equation is just used for the animation curve to change the progress from 0 to 1. And so the spring offset is always the same and it’s equal to 1, and the fromValue and toValue values are ignored.

Advanced Animations with UIKit

UISpringTimingParameters

The third way to create a spring animation, starting with iOS 10, is UISpringTimingParameters. There are two ways to create UISpringTimingParameters:

And the interesting thing here is that the behavior of UISpringTimingParameters will be different, depending on which constructor you use.

If you create UISpringTimingParameters using a constructor with mass, stiffness, and damping, the animation duration is calculated automatically. And the duration we set will be ignored:

This is also done for versatility, since you can do anything in the animations block. And so the offset here is set for progress from 0 to 1. But even if you know the offset yourself and know how to calculate the animation duration, you can’t set it manually.

If you create UISpringTimingParameters using dampingRatio, then the duration won’t be calculated automatically, and you will need to set it:

But the problem here is that we don’t have enough information to calculate it, as in the case of UIView.animate: there is no mass, no stiffness, and no spring offset.

Spring Animation with zero offset

A common problem with all spring animations in iOS is that they don’t work with zero offset (fromValue == toValue). That is, if you want to make such an animation by pushing the circle from its place, then you won’t succeed:

Spring Animation with Zero Displacement

And this code won’t do anything, even though you set the initialSpringVelocity:

And even if you add a frame assignment, nothing changes:

And as we will see later, to implement bounce for SimpleScrollView, you will need a zero-offset animation. As a result, spring animations from the iOS SDK aren’t suitable for us, so we will use our own implementation.

Spring animation implementation

To parameterize the animation, we will use the SpringTimingParameters structure:

  • spring — spring parameters;
  • displacement;
  • initialVelocity;
  • threshold — the threshold in points for finding the damping duration.

Using our formulas, we will find the damping duration and the equation of motion (value:at:):

More on GitHub

Now let’s look at how bounce will work and how the transition from deceleration to spring animation will be implemented:

  • define the bound of the scroll view content;
  • when we take our finger away inside the content, we know the current contentOffset and the velocity of the gesture;
  • we can use them to find the point where the scroll would stop (destination);
  • if we understand that we will collide with a bound, we will find the point of intersection with the bound, and calculate how long it will take before the collision and the velocity of the scroll at this moment;
  • and then we start the deceleration animation and at the moment of collision with the boundary, run the spring animation with an offset of 0 and with velocity at the moment of collision.

Но перед тем, как реализовать этот алгоритм, расширим DecelerationTimingParameters, добавив две вспомогательные функции: duration:to:, чтобы найти время до пересечения с границей, и velocity:at:, чтобы найти скорость в момент столкновения с границей:

But before implementing this algorithm, let’s expand DecelerationTimingParameters by adding two auxiliary functions:

  • duration:to: to find the time before crossing the bound;
  • velocity:at: to find the velocity at the moment of collision with the bound.

Here is our handlePanRecognizer function that processes gestures:

Now we need to improve the startDeceleration function, which is called when the finger is taken away:

  1. initialize DecelerationTimingParameters;
  2. find the point where the scroll stops;
  3. find the intersection point with the content bound;
  4. if we understand that we will encounter a bound, find the time we need before we encounter that bound;
  5. first run the deceleration animation, changing the contentOffset accordingly;
  6. at the moment of collision with the bound calculate the current velocity and call the bounce function.

The bounce function will be implemented as follows:

  1. first, calculate the rest position of the spring, which will be on the bound of the content;
  2. calculate the initial spring offset (in our case, it will be 0, because we are already on the bound);
  3. choose the half-pixel threshold;
  4. choose the parameters of the spring with a damping ratio of 1, so that we don’t have oscillations near the content bound;
  5. initialize SpringTimingParameters;
  6. start the animation, passing the damping duration there. In the animations block, we will call the spring motion equation using the current animation time, and change the contentOffset accordingly.

Here’s what we got:

We take the finger away inside the content, we have a scroll moving to the bound. In that moment, when we are faced with bound, we trigger the animation. Due to the fact that we use the same concept of velocity in deceleration and spring animation, the junction of these two animations turned out to be smooth. We didn’t have to think about how we need to adjust the deceleration, how we need to adjust the animation of the spring.

Rubber Band Effect

Now let’s talk about rubber band effect. Let me remind you that this effect adds scroll resistance at bounds, not only at bounds by coordinates, but also for the scale.

There is no information about rubber band effect in the documentation. So we tried to guess how it works. To begin with, we’ve plotted how contentOffset changes when you shift your finger. And tried to determine which function could best approximate it.

We tried to somehow link this effect to the spring equation. But no matter what we did, we couldn’t get any closer to that graph.

In the end, we decided to simply approximate such a graph by a polynomial. To do this, you can select several points, and find a polynomial that will pass as close to these points as possible. You can do this manually, or you can just go to WolframAlpha and enter our points

quadratic fit {0, 0} {500, 205} {1000, 328} {1500, 409}

and get a polynomial of the second degree:

The resulting polynomial approximates the desired function well:

And we would have stopped there if at some point we didn’t find out that such an effect is generally called the rubber band effect.

And as soon as we found out about it, we pretty quickly came across a tweet, with some formula:

https://twitter.com/chpwn/status/285540192096497664
  • x on the right side is the offset of the finger;
  • c is a ratio of some kind;
  • d is a dimension that is equal to the size of the scroll view.

We built a graph for this function with the ratio of 0.55 and saw that this function perfectly approximates our graph:

Let’s take a closer look at how this formula works. Let’s write it out in more familiar terms:

Let’s build several graphs for different ratios and choose 812 as d (height of iPhone X):

The graphs show that the d ratio affects the stiffness of the rubber band: the smaller this ratio, the more rigid the effect behavior, and the more effort we need to make to shift the contentOffset.

From the function we see that when x tends to infinity, our function tends to d, but it is always less than d:

This formula is quite convenient because you can use it to set the maximum offset using the d parameter. Using this formula, you can ensure that the scroll view content is always on the screen.

This is the only formula that is used for rubber band effect, so let’s improve our SimpleScrollView.

Rubber Band Effect Implementation

Let’s declare the function rubberBandClamp, which will be a regular repetition of the function from the tweet:

Then, for convenience, add the rubberBandClamp function, passing limits there:

This function will work as follows:

  • if x falls within the limits, nothing will happen to x;
  • as soon as x starts going beyond these limits, rubberBandClamp will be applied.

We will need this function for the scroll view, because we only need to use rubberBandClamp when we go beyond the content bound.

Let’s extend this function to a two-dimensional case and create a RubberBand structure:

  • we will use the scroll view bound as bounds (contentOffset bound);
  • as dimensions — the scroll view size;
  • clamp method will work as follows: when the passed point is inside the bounds, nothing will happen, and as soon as we start to go beyond these bounds, then we will apply the rubber band effect.

Here is our handlePanRecognizer function that handles gestures:

And now we are interested in the clampOffset function:

Now the function just clamp the passed contentOffset and blocks movement on the bounds.

To add a rubber band effect, you need to create a RubberBand structure and pass the scroll view size as dimensions, and the contentOffset bound as bounds, and then call the clamp function.

But that’s not all. Previously, when we took our finger away, we immediately started deceleration animation. But now the scroll can go beyond the content bounds. When it goes out of bounds, you no longer need to run the deceleration animation. Instead, you just need to go back to the nearest bound, that is, just call the bounce function .

Let’s put this functionality in the completeGesture function, which we will call when processing the .ended state in the handlePanRecognizer function:

And the implementation of the function itself will be quite simple:

  • when contentOffset is inside the bound, i.e. when we take the finger away inside the bound, then the startDeceleration is simply called;
  • if we take the finger away outside the bound we immediately launch bounce without launching deceleration and scroll up to the nearest bound.

Visually it looks like this:

We’ve reviewed all three scroll view mechanics and fully implemented the scroll behavior. A scroll for Yandex.Metro is implemented in the same way, but there are small but very important differences:

⚠️ in the example for SimpleScrollView the animation for contentOffset is run enterely for the sake of simplicity, but it is more correct to run the animations separately for each of the components: separately for x, separately for y, and separately for scale;

⚠️ in SimpleScrollView, the bounce function is called twice: when colliding with the bound, and when taking your finger away outside of the content. For more correct behavior, I recommend implementing these two cases separately with different stiffness parameters (the mass and damping ratio can be left the same).

Examples

Implementing a scroll is a fairly rare task, which we only did because we couldn’t use SDK, and so I’d like to tell you a little bit about other examples where you can use these same functions.

Drawer. Switching between states

Let's talk about the pull-out drawer that we use in Yandex.Metro, which is used in some Apple apps (Maps, Stocks, Find My, and so on).

There are three states: middle, expanded, and collapsed. The drawer never stops in an intermediate state. As soon as you take your finger away, it should scroll to one of the anchor points. And the main task here is how to find the desired anchor point at the moment of taking the finger away.

You can just take the nearest one. But then it will be difficult to swipe the drawer to the upper state, because you will need to take your finger away at the top:

Nearest Anchor Point

And with this approach, it will seem that the drawer is flying somewhere from under your finger.

So we need to take the velocity of the gesture into account somehow. There are different ways to implement this algorithm, but the most successful solution was shown at WWDC, at the presentation of Designing Fluid Interfaces. There was a slightly different example, but it can be projected to our case. The idea is as follows.

  • when the finger is taken away, the position of the drawer and the velocity of the gesture are known;
  • using them, we find the point where the drawer would have come (the drawer projection);
  • find the nearest anchor point to this projection;
  • scroll to the found anchor point.

To find the drawer projection, you can use the destination of decelerating movement formula:

Let’s describe the project function, which we will pass the current position of the drawer (value), the initial velocity of the gesture, and the decelerationRate. The function will return the drawer projection, i.e. the point where the drawer would have come:

The algorithm will look like this:

  • first, choose DecelerationRate.normal;
  • find the projection;
  • find the nearest anchor;
  • change the position of the drawer animated.
Linear Animation

But the animation is not quite successful. As we use constant duration, the animation looks unnatural, because it doesn’t take into account either the velocity of the gesture or the distance you need to scroll to the anchor point. And here the spring animation can help us. It will work as follows:

  • when the finger is taken away, the position of the drawer and the velocity of the gesture are known;
  • using them, we find the point where the drawer would have come (the drawer projection);
  • find the anchor point nearest to this projection;
  • create the animation of the spring whose resting state is the anchor point, and the initial offset is the distance from the anchor point to the drawer.

Given these changes, the completeGesture function will be as follows:

And it will look like this:

Since we used the ratio of 1, there are no jumps near the anchor point. If we choose a ratio close to 0, we can achieve this effect:

Overall, this approach with the search for projections can be used for the two-dimensional case:

PiP | Example on GitHub

This is an example of a Picture in Picture (or PiP) that is used in FaceTime or Skype. This is the example used to demonstrate the algorithm at WWDC. There is also a fixed set of states and an animated transition between them that takes into account the gesture.

You can also use projection search and spring animation to implement unusual pagination: with different page sizes or with different pagination behavior, as in the new App Store.

iOS 13 App Store

Here pagination is used with oscillations. In standard UIScrollView pagination, the page sizes are always the same, and pagination behavior cannot be controlled.

Drawer. Rubber Band Effect

We can improve our drawer by adding a rubber band effect to it. The question may arise: why do I need to add this effect to the drawer at all? In iOS, everything related to scrolling has a rubber band effect in one way or another. If the scroll freezes, it seems that the app is frozen. By adding this effect to the drawer, we will make it more responsive.

But the formula for rubber band effect here needs to be used a little differently:

  • as x, let’s take the coordinate of the drawer, where the zero position is the upper bound of the content;
  • let’s use the distance to the upper edge of the screen as the dimension, thus ensuring that we can’t scroll the drawer far up, off the screen.

The same applies to the collapsed case:

  • as x, let’s take the position of the drawer, where the zero position is the lower bound of the content;
  • and take the distance to the bottom of the screen as the dimension, thus ensuring that we can’t scroll the drawer off the screen or hide it at all.

Metro Map. Scale

Rubber band effect can be used to restrict any values: coordinates, scale, color, rotation, etc. In the Metro, we added this effect for the scale:

Rubber Band Effect for Scaling

Conclusion

If you need to make a smooth transition between states, and these states can be not only coordinates (it can be color, rotation, or alpha), then you can use projection search and spring animation.

If you want to make a smooth bound of some state, you can use rubber band effect.

A little more about why it is sometimes important to understand how a particular mechanics works and what formulas are used there. After all, for the rubber band effect, we found some approximation in the form of a polynomial, which is quite close to our desired formula.

The difference between the original formula and the approximation is that the original formula has the c ratio, with which you can predictably change the rubber band effect stiffness. In order to achieve the same in an approximate formula, you will have to somehow select the ratios for x.

The original formula also has the d parameter that regulates the maximum offset and you can’t do this with an approximation at all.

When implementing bounce, we used the deceleration animation and then the spring animation. Due to the fact that we used the same concept of velocity, the junction of the two animations automatically turned out to be smooth. We didn’t have to adjust the deceleration separately, or adjust the spring parameters separately. This allows us to change the deceleration behavior and bounce behavior independently in the future, and the joint will always be smooth and it will look like a single solid animation.

If at some point you want to understand how some mechanics work and what formulas are used there, then I would recommend trying to find the original formula, because it will simplify your life in the future.

📚 All code examples, including PiP and TimerAnimation, can be found in the repository: https://github.com/super-ultra/ScrollMechanics.

🚀 The pull-out drawer is available as a pod: https://github.com/super-ultra/UltraDrawerView.

👨‍💻 I also recommend watching Designing Fluid Interfaces and Advanced Animations with UIKit.

--

--