<?xml version="1.0" encoding="utf-8" ?><rss version="2.0" xmlns:tt="http://teletype.in/" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:media="http://search.yahoo.com/mrss/"><channel><title>Swift Discipline</title><generator>teletype.in</generator><description><![CDATA[https://medium.com/@esskeetit]]></description><image><url>https://img1.teletype.in/files/c4/56/c456c85c-dd71-4ff6-a529-de0ebcda7c27.png</url><title>Swift Discipline</title><link>https://teletype.in/@swift-discipline</link></image><link>https://teletype.in/@swift-discipline?utm_source=teletype&amp;utm_medium=feed_rss&amp;utm_campaign=swift-discipline</link><atom:link rel="self" type="application/rss+xml" href="https://teletype.in/rss/swift-discipline?offset=0"></atom:link><atom:link rel="next" type="application/rss+xml" href="https://teletype.in/rss/swift-discipline?offset=10"></atom:link><atom:link rel="search" type="application/opensearchdescription+xml" title="Teletype" href="https://teletype.in/opensearch.xml"></atom:link><pubDate>Sat, 11 Apr 2026 13:16:32 GMT</pubDate><lastBuildDate>Sat, 11 Apr 2026 13:16:32 GMT</lastBuildDate><item><guid isPermaLink="true">https://teletype.in/@swift-discipline/how-uiscrollview-works</guid><link>https://teletype.in/@swift-discipline/how-uiscrollview-works?utm_source=teletype&amp;utm_medium=feed_rss&amp;utm_campaign=swift-discipline</link><comments>https://teletype.in/@swift-discipline/how-uiscrollview-works?utm_source=teletype&amp;utm_medium=feed_rss&amp;utm_campaign=swift-discipline#comments</comments><dc:creator>swift-discipline</dc:creator><title>How UIScrollView works</title><pubDate>Sun, 25 May 2025 19:39:30 GMT</pubDate><media:content medium="image" url="https://img3.teletype.in/files/ee/f7/eef79164-c0db-495e-9e71-f045174e8300.png"></media:content><description><![CDATA[<img src="https://img2.teletype.in/files/d8/ea/d8ea7a6e-0bc0-4767-bfd2-f0184a32dea9.jpeg"></img>How to recreate UIScrollView behavior from scratch: three core mechanics — deceleration, bounce animation, and edge resistance.]]></description><content:encoded><![CDATA[
  <figure id="E7LB" class="m_retina">
    <img src="https://img2.teletype.in/files/d8/ea/d8ea7a6e-0bc0-4767-bfd2-f0184a32dea9.jpeg" width="1160" />
  </figure>
  <p id="0f7a">Yandex.Metro on both iOS and Android uses the shared MetroKit library, written in C++. In particular, MetroKit has a <code>SchemeView</code> for displaying the metro map. And we were faced with the task of implementing a scroll for this scheme. We chose <code>UIScrollView</code> as a reference since we found its behavior the most natural and appropriate.</p>
  <p id="a653">We divided the development process into three mechanics, three stages of implementing the <code>UIScrollView</code> behavior. The first of them is deceleration:</p>
  <p id="Hrip"></p>
  <figure id="bRwU" class="m_retina" data-caption-align="center">
    <img src="https://img4.teletype.in/files/7f/65/7f65a7ad-bdb3-4dcf-8e63-0e73a42c701a.gif" width="480" />
    <figcaption>Deceleration</figcaption>
  </figure>
  <p id="Lixh">The second one is spring animation for implementing bounces on bounds:</p>
  <figure id="r1lE" class="m_retina" data-caption-align="center">
    <img src="https://img2.teletype.in/files/1e/67/1e671fb0-8278-45a6-b18e-0a98eb6a160c.gif" width="480" />
    <figcaption>Spring Animation</figcaption>
  </figure>
  <p id="bFv5">The third mechanic is the so-called rubber band effect for implementing scroll resistance at bounds:</p>
  <figure id="lre5" class="m_retina" data-caption-align="center">
    <img src="https://img1.teletype.in/files/c8/7d/c87db078-c86e-4119-b8d9-2ed12fdfae9d.gif" />
    <figcaption>Rubber Band Effect</figcaption>
  </figure>
  <p id="wBcS">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.</p>
  <h1 id="8fc0">Test Example</h1>
  <p id="d434">Let’s look at the test <code>SimpleScrollView</code>, which we will use as an example to consider all the mechanics, all the formulas, and which we will gradually improve.</p>
  <p id="adcd">Similar to <code>UIScrollView</code>, it will have <code>contentView</code>, <code>contentSize</code>, and <code>contentOffset:</code></p>
  <figure>
    <script src="https://gist.github.com/super-ultra/5df44acb8de0ec0e9bffb756ee6fddc4.js"></script>
  </figure>
  <p id="e1d2">To implement gestures, we will use <code>UIPanGestureRecognizer</code> and the <code>handlePanRecognizer</code> function</p>
  <figure>
    <script src="https://gist.github.com/super-ultra/fc6c37e359c3311b4b8708707f86fb3a.js"></script>
  </figure>
  <p id="9a5c"><code>SimpleScrollView</code> will have one of two states:</p>
  <ul id="PSdV">
    <li id="4690"><code>.default</code> when nothing happens;</li>
    <li id="3bea"><code>.dragging</code> when we scroll.</li>
  </ul>
  <figure>
    <script src="https://gist.github.com/super-ultra/8967d9c55a06c3eacf0270bf16693466.js"></script>
  </figure>
  <p id="lpWZ">The implementation of <code>handlePanRecognizer</code> will look like this:</p>
  <figure>
    <script src="https://gist.github.com/super-ultra/412a8d189501b1ae5a05dd9ef41eff9f.js"></script>
  </figure>
  <ul id="1xYr">
    <li id="300a">when the <code>UIPanGestureRecognizer</code> enters <code>.began</code> state, we set the <code>.dragging</code> state.</li>
    <li id="f28f">when it enters the <code>.changed</code> state, we calculate translation and change the <code>contentOffset</code> of our scroll view accordingly. At the same time, we call the <code>clampOffset</code> function so that we don’t go beyond the content bounds.</li>
    <li id="d9ce">when in the <code>.ended</code> state, we just return <code>SimpleScrollView</code> to the <code>.default</code>state.</li>
  </ul>
  <p id="cff3">Let’s set the <code>contentOffsetBounds</code> auxiliary property that defines the <code>contentOffset</code> bounds using the current <code>contentSize</code>. As well as the <code>clampOffset</code> function, which restricts <code>contentOffset</code> using these bounds.</p>
  <figure>
    <script src="https://gist.github.com/super-ultra/bfb07888bceb496f1ecdd692be545de6.js"></script>
  </figure>
  <p id="AInK">And now we have a simple implementation of <code>SimpleScrollView</code> ready.</p>
  <p id="YGgv"></p>
  <p id="6WwC">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 <code>SimpleScrollView.</code></p>
  <h1 id="10ce">Deceleration</h1>
  <p id="b0b7">Let’s start with deceleration.</p>
  <p id="c94c">There is no information in the SDK about how deceleration is implemented. <code>UIScrollView</code> has a <code>DecelerationRate</code>, which can be either <code>.normal</code>, or <code>.fast</code>, and you can use it to adjust the velocity of scroll deceleration. <a href="https://developer.apple.com/documentation/uikit/uiscrollview/1619438-decelerationrate" target="_blank">The documentation</a> states that <code>DecelerationRate</code> determines the rate of scroll deceleration.</p>
  <figure>
    <script src="https://gist.github.com/super-ultra/2929bd94fceb0ff100b1799f38bd29f9.js"></script>
    <figcaption>UIScrollView | <a href="https://developer.apple.com/documentation/uikit/uiscrollview" target="_blank">https://developer.apple.com/documentation/uikit/uiscrollview</a></figcaption>
  </figure>
  <p id="7ab0">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 <a href="https://developer.apple.com/videos/play/wwdc2018/803/" target="_blank">Designing Fluid Interfaces</a> at WWDC.</p>
  <figure>
    <script src="https://gist.github.com/super-ultra/1e017e807847c87f3c70669e069171b6.js"></script>
  </figure>
  <p id="a106">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.</p>
  <h3 id="21e7">Velocity</h3>
  <p id="688d">Let’s try to guess how deceleration can work and what <code>DecelerationRate</code> can mean overall. <a href="https://developer.apple.com/documentation/uikit/uiscrollview/1619438-decelerationrate" target="_blank">The documentation</a> says that this is</p>
  <blockquote id="NjjU">A floating-point value that determines the rate of deceleration after the user lifts their finger.</blockquote>
  <p id="eacb">We can assume that this rate indicates how much the scroll velocity will change after one millisecond (all <code>UIScrollView</code> values are expressed in milliseconds, unlike <code>UIPanGestureRecognizer</code>).</p>
  <p id="25de">If at the moment of taking the finger away we had the velocity <em>v₀</em> and we chose <code>DecelerationRate.fast</code>, then</p>
  <ul id="7hc1">
    <li id="d953">after 1 millisecond velocity will be <em>0.99</em> of <em>v₀</em>;</li>
    <li id="fca0">after 2 milliseconds velocity will be <em>0.99²</em> of <em>v₀</em>;</li>
    <li id="dde0">after k seconds, velocity will be <em>0. 99¹⁰⁰⁰k</em> of <em>v₀</em>.</li>
  </ul>
  <figure id="JjXw" class="m_retina">
    <img src="https://img1.teletype.in/files/8a/78/8a78d948-8587-4230-84f9-30796789d04d.jpeg" width="800" />
  </figure>
  <p id="nABX">As a result, we got a formula for the velocity of decelerating movement:</p>
  <figure id="f8HF" class="m_retina">
    <img src="https://miro.medium.com/v2/resize:fit:1044/1*14idOeQRwLyTgIqhWfB84g@2x.png" width="261" />
  </figure>
  <h3 id="8a66">Equation of motion</h3>
  <p id="8cab">The velocity formula cannot be used to implement deceleration. We need to find the equation of motion: the dependence of the coordinate on time <em>x(t)</em>. And the velocity formula will help us to find the equation of motion, we just need to integrate it:</p>
  <figure id="fXPE" class="m_retina">
    <img src="https://miro.medium.com/v2/resize:fit:1400/1*uyj9KLzqGHfP4NLcwtUT2g@2x.png" width="423" />
  </figure>
  <p id="bb2d">Then substitute our velocity formula for <em>v(x)</em>, integrate it and we get:</p>
  <figure id="wpqM" class="m_retina">
    <img src="https://miro.medium.com/v2/resize:fit:1400/1*YB-2jEkHBc0Gxx6panS3GQ@2x.png" width="385" />
  </figure>
  <h3 id="2935">Endpoint equation</h3>
  <p id="5bc0">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 <em>t</em> to infinity. Since we have <em>d</em> less than one, and <em>d¹⁰⁰⁰t</em> converges to zero, we get:</p>
  <figure id="oa2j" class="m_retina">
    <img src="https://miro.medium.com/v2/resize:fit:1310/1*pdWOdhdo8p_0DlSzC-Mujg@2x.png" width="327.5" />
  </figure>
  <p id="e4e5">Now let’s compare the found formula with Apple’s formula. Let’s write it out in the same notation:</p>
  <figure id="qGnX" class="m_retina">
    <img src="https://miro.medium.com/v2/resize:fit:1396/1*At8qk7hRvsMYrhUhGlzQ9A@2x.png" width="349" />
  </figure>
  <p id="2684">And we see that the formulas are slightly different in the right-hand parts:</p>
  <figure id="n2bd" class="m_retina" data-caption-align="center">
    <img src="https://miro.medium.com/v2/resize:fit:1168/1*SUfcYSezgnITqKx6M3perg@2x.png" width="292" />
    <figcaption>Ours and Apple’s</figcaption>
  </figure>
  <p id="dbd8">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:</p>
  <figure id="jhSH" class="m_retina" data-caption-align="center">
    <img src="https://miro.medium.com/v2/resize:fit:1400/1*ql_cjYnCPRxx9MbsJjo2lw@2x.png" width="475" />
    <figcaption>Natural Logarithm | <a href="https://en.wikipedia.org/wiki/Natural_logarithm#Series" target="_blank">https://en.wikipedia.org/wiki/Natural_logarithm#Series</a></figcaption>
  </figure>
  <p id="6063">If we plot the graphs of these functions, we will see that when approaching one, they almost coincide:</p>
  <figure id="ISVZ" class="m_retina">
    <img src="https://miro.medium.com/v2/resize:fit:1400/1*UmwnQhrshso3ombCrHsDvw@2x.png" width="700" />
  </figure>
  <p id="2180">Let me remind you that the standard <code>DecelerationRate</code> values are very close to 1, which means that this optimization by Apple is quite correct.</p>
  <h3 id="5cf8"><strong>Deceleration time</strong></h3>
  <p id="98d1">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.</p>
  <p id="3b55">If we plot the equation of motion, we can see that the function is infinitely close to the end point <em>X</em>. 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.</p>
  <figure id="xdIe" class="m_retina">
    <img src="https://miro.medium.com/v2/resize:fit:1400/1*GCuJkYyIxgjQfMKdyEzruA@2x.png" width="700" />
  </figure>
  <p id="2a78">Therefore, we can reformulate our problem as follows: we find a time <em>T</em> after which the function is close enough to the end point <em>X</em> (by some small distance <em>ε</em>). In practice, half a pixel may be taken as an example.</p>
  <p id="889b">Let’s find a <em>T</em> at which the distance to the end point is equal to <em>ε</em>:</p>
  <figure id="N5GF" class="m_retina">
    <img src="https://miro.medium.com/v2/resize:fit:948/1*_F2z9tnQ__YzhdPdf2g6GQ@2x.png" width="237" />
  </figure>
  <p id="bc9b">Substitute our formulas for <em>x</em> and <em>X</em> and get the formula for the time of decelerating motion:</p>
  <figure id="qvIk" class="m_retina">
    <img src="https://miro.medium.com/v2/resize:fit:1352/1*33reIrQTZFQgGMYpRHgfQg@2x.png" width="338" />
  </figure>
  <p id="b880">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.</p>
  <p id="2bc3">Now let’s improve our <code>SimpleScrollView</code> by adding deceleration to it.</p>
  <h1 id="97c7">Deceleration implementation</h1>
  <p id="e8c8">To begin with, let’s describe the <code>DecelerationTimingParameters</code> structure, which will contain all the necessary information to create an animation when you take your finger away:</p>
  <figure>
    <script src="https://gist.github.com/super-ultra/61fb04e1d744e15c6463964ab245f92c.js"></script>
  </figure>
  <ul id="bC1t">
    <li id="c013"><code>initialValue</code> is the initial <code>contentOffset</code> — the point where we took our finger away;</li>
    <li id="4448"><code>initialVelocity</code> is the velocity of the gesture;</li>
    <li id="4cdf"><code>decelerationRate</code> is the deceleration rate;</li>
    <li id="8ea5"><code>threshold</code> is the threshold for finding the deceleration time.</li>
  </ul>
  <p id="cd85">Using our formulas, we will find the point where the scroll stops:</p>
  <figure>
    <script src="https://gist.github.com/super-ultra/c96043c6035b2495c503a30d2c71fba6.js"></script>
  </figure>
  <p id="c039">Deceleration duration:</p>
  <figure>
    <script src="https://gist.github.com/super-ultra/603f55267288ccff933ad20ab7f376d7.js"></script>
  </figure>
  <p id="d6b9">And the equation of motion:</p>
  <figure>
    <script src="https://gist.github.com/super-ultra/34c203e12cca97e32d716b8f48e0e290.js"></script>
  </figure>
  <p id="f22d">We will use <code>TimerAnimation</code>, 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:</p>
  <figure>
    <script src="https://gist.github.com/super-ultra/25c927c8ae8dd61bbb926998621dca63.js"></script>
  </figure>
  <p id="3eee">We will use the <code>animations</code> block to use the current time to call the equation of motion and change the <code>contentOffset</code> accordingly. The <code>TimerAnimation</code>implementation can be found in <a href="https://github.com/super-ultra/ScrollMechanics/blob/master/ScrollMechanics/Sources/Utils/TimerAnimation.swift" target="_blank">the repository</a>.</p>
  <p id="8279">And now we will improve the gesture processing function:</p>
  <figure>
    <script src="https://gist.github.com/super-ultra/412a8d189501b1ae5a05dd9ef41eff9f.js"></script>
  </figure>
  <p id="8e5f">Deceleration should start when the finger is taken away. Therefore, when the <code>.ended</code> state comes, we will call the <code>startDeceleration</code> function, passing the velocity of the gesture to it:</p>
  <figure>
    <script src="https://gist.github.com/super-ultra/a01bad4c6b73e206ed1a802389831e19.js"></script>
  </figure>
  <p id="Ewy9">The implementation of <code>startDeceleration</code> will be as follows:</p>
  <figure>
    <script src="https://gist.github.com/super-ultra/ff4bca0722b345047b99401d2d91632e.js"></script>
  </figure>
  <ul id="YLS3">
    <li id="f585">choose <code>DecelerationRate.normal</code> and the threshold of half a pixel;</li>
    <li id="9e96">initialize <code>DecelerationTimingParameters</code>;</li>
    <li id="abfd">run the animation, passing the animation time there, and we will call the equation of motion and update the <code>contentOffset</code> in the <code>animations</code>block, using the current time.</li>
  </ul>
  <p id="6463">Here’s what we got:</p>
  <figure id="UBQi" class="m_retina" data-caption-align="center">
    <img src="https://img4.teletype.in/files/7f/65/7f65a7ad-bdb3-4dcf-8e63-0e73a42c701a.gif" width="480" />
    <figcaption>Deceleration</figcaption>
  </figure>
  <p id="751c">This is all about deceleration, and now let’s talk about the spring animation.</p>
  <h2 id="9c17">Spring Animation</h2>
  <p id="00a2">Let me remind you that we used the spring animation to implement bounce on bounds.</p>
  <p id="4ba5">There is much more information about the spring animation, as opposed to deceleration. It is based on the damped <a href="https://en.wikipedia.org/wiki/Damping_ratio" target="_blank">oscillations of the spring</a>. 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.</p>
  <p id="0329">Most often the spring is parameterized by:</p>
  <ul id="QxiJ">
    <li id="57c5">mass (<em>m)</em></li>
    <li id="083e">stiffness (<em>k</em>)</li>
    <li id="7c25">damping (<em>d</em>)</li>
  </ul>
  <p id="48d4">The equations of motion of the spring looks like this. It also depends on the mass, stiffness, and damping:</p>
  <figure id="mEWN" class="m_retina">
    <img src="https://miro.medium.com/v2/resize:fit:1400/1*w7KcQxshpIosqOJdRGcJ1A@2x.png" width="352" />
  </figure>
  <p id="e21a">Sometimes, instead of damping, you can find a damping ratio (<em>ζ</em>). They are linked by the following formula:</p>
  <figure id="Yfr4" class="m_retina">
    <img src="https://miro.medium.com/v2/resize:fit:688/1*wfIxFvuz6tc7t8WkOoKp7Q@2x.png" width="172" />
  </figure>
  <h3 id="7244">Damping Ratio</h3>
  <p id="f371">The most interesting parameter is the damping ratio. You can use it to determine what the animation will look like.</p>
  <figure id="nbYi" class="m_retina" data-caption-align="center">
    <img src="https://miro.medium.com/v2/resize:fit:1400/1*WC6xD5Doveqo6PbaLpuY6g@2x.gif" width="480" />
    <figcaption>Damping Ratio: 0.1, 0.5 and 1.0</figcaption>
  </figure>
  <p id="1fe4">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.</p>
  <figure id="3uRQ" class="m_retina">
    <img src="https://miro.medium.com/v2/resize:fit:1400/1*dRmG3LQbTSfl1ISm-oQFCA.png" width="660" />
  </figure>
  <p id="d3d7">Depending on the ratio, the damped vibrations are divided into three types:</p>
  <ul id="pFAb">
    <li id="f33e"><em><strong>0 &lt; ζ &lt; 1</strong> — </em>underdamped. In this case, there are jumps near the rest state. The closer the ratio is to zero, the more pronounced the jumps are.</li>
    <li id="4ad1"><strong><em>ζ = 1 </em></strong>— 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.</li>
    <li id="16be"><strong><em>ζ &gt; 1</em></strong> — overdamped. In this case, the oscillations simply fade exponentially. This case is rarely used, so we won’t consider it.</li>
  </ul>
  <h3 id="5124">Equation of motion</h3>
  <p id="87f1">As we already know, the equation of motion in general form looks as follows:</p>
  <figure id="eE56" class="m_retina">
    <img src="https://miro.medium.com/v2/resize:fit:1400/1*w7KcQxshpIosqOJdRGcJ1A@2x.png" width="352" />
  </figure>
  <p id="0ebe">In practice, it is difficult to use it, so we need to find a solution to this equation in the form of <em>x(t)</em>. 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.</p>
  <p id="b42e"><strong>Underdamped (0 &lt; ζ &lt; 1)</strong></p>
  <p id="fd5a">In the case when we have a ratio less than one (underdamped), the solution of the equation looks like this:</p>
  <figure id="jZ4I" class="m_retina">
    <img src="https://miro.medium.com/v2/resize:fit:1400/1*qzzjRZ5fVDsTrqJBqp0rIQ@2x.png" width="556" />
  </figure>
  <ul id="u3vV">
    <li id="7c9b"><em>ω’ </em>— <a href="https://en.wikipedia.org/wiki/Damping_ratio" target="_blank">damped natural frequency</a>;</li>
    <li id="12f9"><em>β</em> — the ancillary parameter:</li>
  </ul>
  <figure id="HHmF" class="m_retina">
    <img src="https://miro.medium.com/v2/resize:fit:512/1*50q-ZHgymUdgmyjQs3oVJg@2x.png" width="128" />
  </figure>
  <ul id="f1Dy">
    <li id="1edb">the coefficients <em>C₁</em> and <em>C₂</em> are found using initial conditions: the initial position of the point is <em>x₀</em>, and the initial velocity is <em>v₀</em>:</li>
  </ul>
  <figure id="hPVf" class="m_retina">
    <img src="https://miro.medium.com/v2/resize:fit:1400/1*eS2puM-1MPjFGHHWl4R8OQ@2x.png" width="454" />
  </figure>
  <p id="5050">The sine and cosine on the left side tells us that the oscillation has a period, and the exponent — that the oscillations exponentially fade.</p>
  <p id="0a62">Visually, it looks like this: the amplitude decreases exponentially over time with some frequency due to the sine and cosine:</p>
  <figure id="z4WE" class="m_retina">
    <img src="https://miro.medium.com/v2/resize:fit:1400/1*d8nkir6zws0tkPV-KPcL_w@2x.gif" width="960" />
  </figure>
  <p id="3ba0"><strong>Critically damped <em>(ζ = 1)</em></strong></p>
  <p id="d5f9">Now let’s consider the critically damped case. The equation of motion looks like this:</p>
  <figure id="EGwz" class="m_retina">
    <img src="https://miro.medium.com/v2/resize:fit:1400/1*ILppPRCYLrcWM5Lgox74Yg@2x.png" width="379" />
  </figure>
  <ul id="HIVX">
    <li id="03c5"><em>β</em> — the same auxiliary parameter;</li>
    <li id="63ac">the coefficients <em>C₁</em> and<em> C₂</em> differ from the previous case, but they are also found using the initial conditions:</li>
  </ul>
  <figure id="dDIi" class="m_retina">
    <img src="https://miro.medium.com/v2/resize:fit:1400/1*_BOf1TDiAABSkVGf_Qk4tQ@2x.png" width="450" />
  </figure>
  <p id="a4c1">The graph will be as follows:</p>
  <figure id="mIEY" class="m_retina">
    <img src="https://miro.medium.com/v2/resize:fit:1400/1*h5zoIFekjWPV_feb6IA1Mw@2x.gif" width="960" />
  </figure>
  <p id="5c15">There are no longer any jumps, the amplitude just fades exponentially.</p>
  <h3 id="2a22">Oscillation time</h3>
  <p id="e4be">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:</p>
  <figure id="LiI8" class="m_retina">
    <img src="https://miro.medium.com/v2/resize:fit:1400/1*lY_i6QJc2FrXY3uCCevsqg@2x.png" width="661" />
  </figure>
  <p id="023f">So we reformulate the problem again: we find a time <em>T</em> after which the amplitude is less than some small value of <em>ε</em> (for example, half a pixel).</p>
  <p id="ef96">And using the same reasoning as in the case of deceleration, we first find the time for the underdamped system (<em>0 &lt; ζ &lt; 1)</em>:</p>
  <figure id="Au0n" class="m_retina">
    <img src="https://miro.medium.com/v2/resize:fit:1388/1*S1FbOHvdLEVg1GjyIR2ogg@2x.png" width="347" />
  </figure>
  <p id="b7db">And we find the time for the critically damped system in the same way <em>(ζ = 1)</em>:</p>
  <figure id="64Uq" class="m_retina">
    <img src="https://miro.medium.com/v2/resize:fit:1400/1*N9-B_nJJyjAewESRxIoRhw@2x.png" width="514" />
  </figure>
  <p id="75e8">As a result, we found formulas for implementing spring animation.</p>
  <p id="5548">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?</p>
  <p id="9e2c">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 <code>SimpleScrollView</code>, and it isn’t clear why iOS developers need it.</p>
  <h1 id="2e30">Spring Animation in the iOS SDK</h1>
  <p id="55aa">The iOS SDK does have several ways to create a spring animation, and the easiest one is <a href="https://developer.apple.com/documentation/uikit/uiview/1622594-animate" target="_blank">UIView.animate</a>.</p>
  <h3 id="d37c">UIView.animate</h3>
  <figure>
    <script src="https://gist.github.com/super-ultra/04d594f7feceb2abce24359ad6c9ae38.js"></script>
  </figure>
  <p id="X82M"><code>UIView.animate</code> is parameterized by the <code>dampingRatio</code> and <code>initialSpringVelocity</code>. 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.</p>
  <p id="3b0a">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.</p>
  <p id="ba32">Therefore, the <code>UIView.animate</code> 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.</p>
  <h3 id="5f41">CASpringAnimation</h3>
  <p id="8e76">Another way is <a href="https://developer.apple.com/documentation/quartzcore/caspringanimation" target="_blank">CASpringAnimation</a>:</p>
  <figure>
    <script src="https://gist.github.com/super-ultra/05a5ce8af0ab03ac8cc2e077e4632e6a.js"></script>
  </figure>
  <p id="943c">Although <code>CASpringAnimation</code> is necessary for animating <code>CALayer</code> properties , and it would be at least inconvenient to use it, let’s also talk about it. <code>CASpringAnimation</code> 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.</p>
  <p id="0ab1">But after we’ve learned the formulas, we can extend the <code>CASpringAnimation</code>class and add a constructor that takes the damping ratio:</p>
  <figure>
    <script src="https://gist.github.com/super-ultra/cd73802eba4bb619917274f46d9fb384.js"></script>
  </figure>
  <p id="2481">Also here you need to set the damping duration, as in the case of <code>UIView.animate</code>, but unlike <code>UIView.animate</code>, there is an auxiliary field <code>settlingDuration</code>, which would return the estimated damping duration based on the <code>CASpringAnimation</code> settings:</p>
  <figure>
    <script src="https://gist.github.com/super-ultra/99fa2a28d6f9555c3ca0a42957c7e490.js"></script>
  </figure>
  <p id="5dbd">The problem here is that <code>settlingDuration</code> doesn’t take into account the spring offset in any way, it doesn’t take into account <code>fromValue</code> and <code>toValue</code>in any way. Whatever you set in <code>fromValue</code> and <code>toValue</code>, <code>settlingDuration</code> is always the same. This is done for versatility, because <code>fromValue</code> and <code>toValue</code>can be anything: it can be coordinates or color — and here it’s not clear how to calculate the offset of the spring.</p>
  <p id="20ae">And what actually happens here is the following. You probably know that when calling <code>UIView.animate</code>, you can pass an animation curve as a parameter: for example, <code>.linear</code>, <code>.easeIn</code>, <code>.easeOut</code>, or <code>.easeInOut</code>.</p>
  <p id="8543">And this curve will indicate how the animation progress will change over time from 0 to 1.</p>
  <figure id="yxMX" class="m_retina" data-caption-align="center">
    <img src="https://miro.medium.com/v2/resize:fit:1400/1*DEmt2wH5osxABuego947sA@2x.png" width="700" />
    <figcaption><a href="http://developer.apple.com/videos/play/wwdc2017/230/" target="_blank">Advanced Animations with UIKit</a></figcaption>
  </figure>
  <p id="73da">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 <code>fromValue</code> and <code>toValue</code> values are ignored.</p>
  <figure id="ZnAS" class="m_retina" data-caption-align="center">
    <img src="https://miro.medium.com/v2/resize:fit:1400/1*937AWyAK77eVAQ2UaQI8Rw@2x.png" width="700" />
    <figcaption><a href="http://developer.apple.com/videos/play/wwdc2017/230/" target="_blank">Advanced Animations with UIKit</a></figcaption>
  </figure>
  <h3 id="8da2">UISpringTimingParameters</h3>
  <p id="a739">The third way to create a spring animation, starting with iOS 10, is <a href="https://developer.apple.com/documentation/uikit/uispringtimingparameters" target="_blank">UISpringTimingParameters</a>. There are two ways to create <code>UISpringTimingParameters</code>:</p>
  <figure>
    <script src="https://gist.github.com/super-ultra/21ba3e431206cd15579bf41f238e2ad5.js"></script>
  </figure>
  <p id="a008">And the interesting thing here is that the behavior of <code>UISpringTimingParameters</code> will be different, depending on which constructor you use.</p>
  <p id="65dd">If you create <code>UISpringTimingParameters</code> using a constructor with <code>mass</code>, <code>stiffness</code>, and <code>damping</code>, the animation duration is calculated automatically. And the <code>duration</code> we set will be ignored:</p>
  <figure>
    <script src="https://gist.github.com/super-ultra/5b1496eafc8a9ddf9f2b3b8fbe5dcc97.js"></script>
  </figure>
  <p id="0373">This is also done for versatility, since you can do anything in the <code>animations</code>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.</p>
  <p id="d3be">If you create <code>UISpringTimingParameters</code> using <code>dampingRatio</code>, then the duration won’t be calculated automatically, and you will need to set it:</p>
  <figure>
    <script src="https://gist.github.com/super-ultra/e78ab5ce64b7f26f81b078a460a1f288.js"></script>
  </figure>
  <p id="4556">But the problem here is that we don’t have enough information to calculate it, as in the case of <code>UIView.animate</code>: there is no mass, no stiffness, and no spring offset.</p>
  <h3 id="05b6">Spring Animation with zero offset</h3>
  <p id="83bb">A common problem with all spring animations in iOS is that they don’t work with zero offset (<code>fromValue == toValue</code>). That is, if you want to make such an animation by pushing the circle from its place, then you won’t succeed:</p>
  <figure id="or9T" class="m_retina" data-caption-align="center">
    <img src="https://miro.medium.com/v2/resize:fit:320/1*XNUec-Af3WIBrF0VYTHGPA@2x.gif" width="80" />
    <figcaption>Spring Animation with Zero Displacement</figcaption>
  </figure>
  <p id="0e66">And this code won’t do anything, even though you set the <code>initialSpringVelocity</code>:</p>
  <figure>
    <script src="https://gist.github.com/super-ultra/2139195a93a56973ffba570fdeafd74a.js"></script>
  </figure>
  <p id="47e6">And even if you add a frame assignment, nothing changes:</p>
  <figure>
    <script src="https://gist.github.com/super-ultra/1fcac9896b2c4b23ef98f1f74b6732b7.js"></script>
  </figure>
  <p id="38e1">And as we will see later, to implement bounce for <code>SimpleScrollView</code>, 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.</p>
  <h1 id="c6d3">Spring animation implementation</h1>
  <p id="2885">To parameterize the animation, we will use the <code>SpringTimingParameters</code>structure:</p>
  <figure>
    <script src="https://gist.github.com/super-ultra/d2b0b205eb27d31c205b918cdadf75cd.js"></script>
  </figure>
  <ul id="I1I2">
    <li id="233f"><code>spring</code> — spring parameters;</li>
    <li id="5f7a"><code>displacement</code>;</li>
    <li id="56a6"><code>initialVelocity</code>;</li>
    <li id="b212"><code>threshold</code> — the threshold in points for finding the damping duration.</li>
  </ul>
  <p id="28f7">Using our formulas, we will find the damping duration and the equation of motion (<code>value:at:</code>):</p>
  <figure>
    <script src="https://gist.github.com/super-ultra/228d7932903e2663c75045fe037ae0c9.js"></script>
    <figcaption>More on <a href="https://github.com/super-ultra/ScrollMechanics/blob/master/ScrollMechanics/Sources/SpringTimingParameters.swift" target="_blank">GitHub</a></figcaption>
  </figure>
  <p id="65ac">Now let’s look at how bounce will work and how the transition from deceleration to spring animation will be implemented:</p>
  <ul id="HftT">
    <li id="b5c3">define the bound of the scroll view content;</li>
    <li id="aede">when we take our finger away inside the content, we know the current <code>contentOffset</code> and the velocity of the gesture;</li>
    <li id="c04d">we can use them to find the point where the scroll would stop (<code>destination</code>);</li>
    <li id="b048">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;</li>
    <li id="ffe8">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.</li>
  </ul>
  <p id="ede2">But before implementing this algorithm, let’s expand <code>DecelerationTimingParameters</code> by adding two auxiliary functions:</p>
  <ul id="BnUF">
    <li id="0adb"><code>duration:to:</code> to find the time before crossing the bound;</li>
    <li id="5920"><code>velocity:at:</code> to find the velocity at the moment of collision with the bound.</li>
  </ul>
  <figure>
    <script src="https://gist.github.com/super-ultra/5f2f37da6db0be5bee5c011c6be2f7cb.js"></script>
  </figure>
  <p id="6pjI">Here is our <code>handlePanRecognizer</code> function that processes gestures:</p>
  <figure>
    <script src="https://gist.github.com/super-ultra/a01bad4c6b73e206ed1a802389831e19.js"></script>
  </figure>
  <p id="7MR2">Now we need to improve the <code>startDeceleration</code> function, which is called when the finger is taken away:</p>
  <figure>
    <script src="https://gist.github.com/super-ultra/9f02444f7428873a3d895908bb912b22.js"></script>
  </figure>
  <ol id="SwM1">
    <li id="e750">initialize <code>DecelerationTimingParameters</code>;</li>
    <li id="e5a2">find the point where the scroll stops;</li>
    <li id="8cd8">find the intersection point with the content bound;</li>
    <li id="f04f">if we understand that we will encounter a bound, find the time we need before we encounter that bound;</li>
    <li id="bc05">first run the deceleration animation, changing the <code>contentOffset</code>accordingly;</li>
    <li id="a62e">at the moment of collision with the bound calculate the current velocity and call the <code>bounce</code> function.</li>
  </ol>
  <p id="d5fc">The <code>bounce</code> function will be implemented as follows:</p>
  <figure>
    <script src="https://gist.github.com/super-ultra/d7eb17c205d9ae8c2f0c697c56867971.js"></script>
  </figure>
  <ol id="BnGz">
    <li id="6b52">first, calculate the rest position of the spring, which will be on the bound of the content;</li>
    <li id="a258">calculate the initial spring offset (in our case, it will be 0, because we are already on the bound);</li>
    <li id="9316">choose the half-pixel threshold;</li>
    <li id="4d9f">choose the parameters of the spring with a damping ratio of 1, so that we don’t have oscillations near the content bound;</li>
    <li id="b9a7">initialize <code>SpringTimingParameters</code>;</li>
    <li id="3ad1">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 <code>contentOffset</code> accordingly.</li>
  </ol>
  <p id="0b2c">Here’s what we got:</p>
  <figure id="8tLP" class="m_retina">
    <img src="https://miro.medium.com/v2/resize:fit:1400/1*Y1lgaGEaOhqmjMQEXj5ZPQ@2x.gif" width="480" />
  </figure>
  <p id="c110">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.</p>
  <h1 id="0ae2">Rubber Band Effect</h1>
  <p id="67c3">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.</p>
  <p id="67b0">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 <code>contentOffset</code> changes when you shift your finger. And tried to determine which function could best approximate it.</p>
  <figure id="i6eM" class="m_retina">
    <img src="https://miro.medium.com/v2/resize:fit:1400/1*e9cga5A_jqLGPyZvCGkiZQ@2x.png" width="700" />
  </figure>
  <p id="b89c">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.</p>
  <p id="caaa">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 <a href="https://www.wolframalpha.com/input/?i=quadratic+fit+%7B0%2C+0%7D+%7B500%2C+205%7D+%7B1000%2C+328%7D+%7B1500%2C+409%7D" target="_blank">WolframAlpha</a> and enter our points</p>
  <pre id="fmIh">quadratic fit {0, 0} {500, 205} {1000, 328} {1500, 409}</pre>
  <p id="6ddf">and get a polynomial of the second degree:</p>
  <figure id="Miz4" class="m_retina">
    <img src="https://miro.medium.com/v2/resize:fit:1400/1*8RnvlsVlJh-6N4pdyyZ_gA@2x.png" width="447" />
  </figure>
  <p id="9084">The resulting polynomial approximates the desired function well:</p>
  <figure id="eXFf" class="m_retina">
    <img src="https://miro.medium.com/v2/resize:fit:1400/1*KzNX-f-1ylzLkYzxvNXfZg@2x.png" width="700" />
  </figure>
  <p id="76e5">And we would have stopped there if at some point we didn’t find out that such an effect is generally called <strong>the</strong> <strong>rubber band effect</strong>.</p>
  <p id="c35a">And as soon as we found out about it, we pretty quickly came across a tweet, with some formula:</p>
  <section style="background-color:hsl(hsl(0,   0%,  var(--autocolor-background-lightness, 95%)), 85%, 85%);">
    <figure id="e6fR" class="m_retina" data-caption-align="center">
      <img src="https://miro.medium.com/v2/resize:fit:1400/1*CbTzR8m12TV4Ke7cRYrdOA@2x.png" width="528" />
      <figcaption><a href="https://twitter.com/chpwn/status/285540192096497664" target="_blank">https://twitter.com/chpwn/status/285540192096497664</a></figcaption>
    </figure>
  </section>
  <ul id="Mavf">
    <li id="d25c"><em>x</em> on the right side is the offset of the finger;</li>
    <li id="d256"><em>c</em> is a ratio of some kind;</li>
    <li id="39ba"><em>d</em> is a dimension that is equal to the size of the scroll view.</li>
  </ul>
  <p id="da7c">We built a graph for this function with the ratio of <code>0.55</code> and saw that this function perfectly approximates our graph:</p>
  <figure id="qDDv" class="m_retina">
    <img src="https://miro.medium.com/v2/resize:fit:1400/1*sXnYxJPNokd7wuMb8Ne3CQ@2x.png" width="700" />
  </figure>
  <p id="13b8">Let’s take a closer look at how this formula works. Let’s write it out in more familiar terms:</p>
  <figure id="WJlJ" class="m_retina">
    <img src="https://miro.medium.com/v2/resize:fit:1360/1*X4ouI4fKagpcga-8F6gSZw@2x.png" width="340" />
  </figure>
  <p id="a01c">Let’s build several graphs for different ratios and choose 812 as d (height of iPhone X):</p>
  <figure id="dlCG" class="m_retina">
    <img src="https://miro.medium.com/v2/resize:fit:1400/1*ci8dnI58UWtckoxvthkdaA@2x.png" width="616" />
  </figure>
  <p id="5b76">The graphs show that the <em>d</em> 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 <code>contentOffset</code>.</p>
  <p id="c544">From the function we see that when <em>x</em> tends to infinity, our function tends to <em>d</em>, but it is always less than <em>d</em>:</p>
  <figure id="o02E" class="m_retina">
    <img src="https://miro.medium.com/v2/resize:fit:404/1*3P0sPjGWGcuVGIAT_mS3tg@2x.png" width="101" />
  </figure>
  <p id="c823">This formula is quite convenient because you can use it to set the maximum offset using the <em>d</em> parameter. Using this formula, you can ensure that the scroll view content is always on the screen.</p>
  <p id="ea76">This is the only formula that is used for rubber band effect, so let’s improve our <code>SimpleScrollView</code>.</p>
  <h1 id="db9d">Rubber Band Effect Implementation</h1>
  <p id="29c0">Let’s declare the function <code>rubberBandClamp</code>, which will be a regular repetition of the function from the tweet:</p>
  <figure>
    <script src="https://gist.github.com/super-ultra/9313eb06a5c73ee5c92e8fb76249953e.js"></script>
  </figure>
  <p id="8d25">Then, for convenience, add the <code>rubberBandClamp</code> function, passing <code>limits</code>there:</p>
  <figure>
    <script src="https://gist.github.com/super-ultra/d94fe49944c2fa53a68da1a49355fa43.js"></script>
  </figure>
  <p id="0642">This function will work as follows:</p>
  <figure id="2CDJ" class="m_retina">
    <img src="https://miro.medium.com/v2/resize:fit:1400/1*rXAWrlUQw2edKyxAnPo6wg@2x.png" width="432" />
  </figure>
  <ul id="oI3d">
    <li id="2f0e">if <code>x</code> falls within the <code>limits</code>, nothing will happen to <code>x</code>;</li>
    <li id="f79d">as soon as <code>x</code> starts going beyond these limits, <code>rubberBandClamp</code> will be applied.</li>
  </ul>
  <p id="a60e">We will need this function for the scroll view, because we only need to use <code>rubberBandClamp</code> when we go beyond the content bound.</p>
  <p id="25fb">Let’s extend this function to a two-dimensional case and create a <code>RubberBand</code>structure:</p>
  <figure>
    <script src="https://gist.github.com/super-ultra/5d8ecc025647e62fdcb9d3296d40de8b.js"></script>
  </figure>
  <ul id="mB3T">
    <li id="7b2a">we will use the scroll view bound as <code>bounds</code> (<code>contentOffset</code> bound);</li>
    <li id="b56e">as <code>dimensions</code> — the scroll view size;</li>
    <li id="9c00"><code>clamp</code> method will work as follows: when the passed <code>point</code> is inside the <code>bounds</code>, nothing will happen, and as soon as we start to go beyond these <code>bounds</code>, then we will apply the rubber band effect.</li>
  </ul>
  <p id="a44a">Here is our <code>handlePanRecognizer</code> function that handles gestures:</p>
  <figure>
    <script src="https://gist.github.com/super-ultra/a01bad4c6b73e206ed1a802389831e19.js"></script>
  </figure>
  <p id="17d3">And now we are interested in the <code>clampOffset</code> function:</p>
  <figure>
    <script src="https://gist.github.com/super-ultra/c17eafbc549ad200a50c025a9195d0ef.js"></script>
  </figure>
  <p id="e5c4">Now the function just clamp the passed <code>contentOffset</code> and blocks movement on the bounds.</p>
  <p id="a8b1">To add a rubber band effect, you need to create a <code>RubberBand</code> structure and pass the scroll view size as <code>dimensions</code>, and the <code>contentOffset</code> bound as <code>bounds</code>, and then call the <code>clamp</code> function.</p>
  <figure>
    <script src="https://gist.github.com/super-ultra/b256324218f3ff99cc0751b135b23698.js"></script>
  </figure>
  <p id="28fa">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 <code>bounce</code> function .</p>
  <p id="5a6d">Let’s put this functionality in the <code>completeGesture</code> function, which we will call when processing the <code>.ended</code> state in the <code>handlePanRecognizer</code> function:</p>
  <figure>
    <script src="https://gist.github.com/super-ultra/f49093e93e6de7410b5834afd64904b0.js"></script>
  </figure>
  <p id="9c0a">And the implementation of the function itself will be quite simple:</p>
  <figure>
    <script src="https://gist.github.com/super-ultra/de44c1476317b4f2fdb5b3bff4206759.js"></script>
  </figure>
  <ul id="dPtY">
    <li id="9ce3">when <code>contentOffset</code> is inside the bound, i.e. when we take the finger away inside the bound, then the <code>startDeceleration</code> is simply called;</li>
    <li id="75e7">if we take the finger away outside the bound we immediately launch <code>bounce</code> without launching deceleration and scroll up to the nearest bound.</li>
  </ul>
  <p id="4502">Visually it looks like this:</p>
  <figure id="wO89" class="m_retina">
    <img src="https://miro.medium.com/v2/resize:fit:1400/1*_g2187lAtqkyrfacbFHRHA@2x.gif" width="480" />
  </figure>
  <p id="5861">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 <strong>but very important</strong> differences:</p>
  <p id="79b4">⚠️ in the example for <code>SimpleScrollView</code> the animation for <code>contentOffset</code> is run enterely for the sake of simplicity, but it is more correct to run the animations <strong>separately for each of the components</strong>: separately for <code>x</code>, separately for <code>y</code>, and separately for <code>scale</code>;</p>
  <p id="b090">⚠️ in <code>SimpleScrollView</code>, the <code>bounce</code> 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).</p>
  <h1 id="f026">Examples</h1>
  <p id="bb98">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.</p>
  <h3 id="48e8">Drawer. Switching between states</h3>
  <p id="a0ca">Let&#x27;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).</p>
  <figure id="imDT" class="m_retina">
    <img src="https://miro.medium.com/v2/resize:fit:1400/1*xIiUlZ452MWfjSIAKSKC9w@2x.gif" width="480" />
  </figure>
  <p id="c84a">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.</p>
  <p id="6971">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:</p>
  <figure id="gf6R" class="m_retina" data-caption-align="center">
    <img src="https://miro.medium.com/v2/resize:fit:1400/1*DCmvLyHZZm-9npzjd3IFrQ@2x.gif" width="480" />
    <figcaption>Nearest Anchor Point</figcaption>
  </figure>
  <p id="ab3d">And with this approach, it will seem that the drawer is flying somewhere from under your finger.</p>
  <p id="fd64">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 <a href="https://developer.apple.com/videos/play/wwdc2018/803/" target="_blank">Designing Fluid Interfaces</a>. There was a slightly different example, but it can be projected to our case. The idea is as follows.</p>
  <figure id="OXnz" class="m_retina">
    <img src="https://miro.medium.com/v2/resize:fit:1400/1*QYKKbSXe-uffJM0D7hfIZQ@2x.gif" width="600" />
  </figure>
  <ul id="Q5dd">
    <li id="f207">when the finger is taken away, the position of the drawer and the velocity of the gesture are known;</li>
    <li id="9439">using them, we find the point where the drawer would have come (the drawer projection);</li>
    <li id="1aba">find the nearest anchor point to this projection;</li>
    <li id="a37b">scroll to the found anchor point.</li>
  </ul>
  <p id="1985">To find the drawer projection, you can use the destination of decelerating movement formula:</p>
  <figure id="FDtR" class="m_retina">
    <img src="https://miro.medium.com/v2/resize:fit:1252/1*OUFI7hlMJDdBFJUX5BSIew@2x.png" width="313" />
  </figure>
  <p id="00c4">Let’s describe the <code>project</code> function, which we will pass the current position of the drawer (<code>value</code>), the initial <code>velocity</code> of the gesture, and the <code>decelerationRate</code>. The function will return the drawer projection, i.e. the point where the drawer would have come:</p>
  <figure>
    <script src="https://gist.github.com/super-ultra/b0e426166f2940db4256d309d46e8e8e.js"></script>
  </figure>
  <p id="9d4f">The algorithm will look like this:</p>
  <figure>
    <script src="https://gist.github.com/super-ultra/7001bc3b10a10e7fb0686c1cee8febba.js"></script>
  </figure>
  <ul id="SqCC">
    <li id="f3bb">first, choose <code>DecelerationRate.normal</code>;</li>
    <li id="f698">find the <code>projection;</code></li>
    <li id="f39a">find the nearest <code>anchor</code>;</li>
    <li id="7fef">change the position of the drawer animated.</li>
  </ul>
  <figure id="71Lb" class="m_retina" data-caption-align="center">
    <img src="https://miro.medium.com/v2/resize:fit:1400/1*QR3O9bzCHSVfZrbIgflVHA@2x.gif" width="480" />
    <figcaption>Linear Animation</figcaption>
  </figure>
  <p id="a2f9">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:</p>
  <figure id="sZuQ" class="m_retina">
    <img src="https://miro.medium.com/v2/resize:fit:1400/1*TtG2roOtkVFGBicWl1TfhQ@2x.gif" width="772" />
  </figure>
  <ul id="MNVf">
    <li id="7740">when the finger is taken away, the position of the drawer and the velocity of the gesture are known;</li>
    <li id="548e">using them, we find the point where the drawer would have come (the drawer projection);</li>
    <li id="c2cb">find the anchor point nearest to this projection;</li>
    <li id="afc6">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.</li>
  </ul>
  <p id="29d8">Given these changes, the <code>completeGesture</code> function will be as follows:</p>
  <figure>
    <script src="https://gist.github.com/super-ultra/b49dfdc33de5b735d4c996db46dba5bc.js"></script>
  </figure>
  <p id="ada6">And it will look like this:</p>
  <figure id="FFQX" class="m_retina">
    <img src="https://miro.medium.com/v2/resize:fit:1400/1*xIiUlZ452MWfjSIAKSKC9w@2x.gif" width="480" />
  </figure>
  <p id="7dfc">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:</p>
  <figure id="Dwgc" class="m_retina">
    <img src="https://miro.medium.com/v2/resize:fit:1400/1*vXcogjNWhivpKXKp89qU7A@2x.gif" width="480" />
  </figure>
  <p id="d42f">Overall, this approach with the search for projections can be used for the two-dimensional case:</p>
  <figure id="pkJi" class="m_retina" data-caption-align="center">
    <img src="https://miro.medium.com/v2/resize:fit:1400/1*isngzbnbAoHZlg1w-x9SkQ@2x.gif" width="480" />
    <figcaption>PiP | <a href="https://github.com/super-ultra/ScrollMechanics/tree/master/PictureInPicture" target="_blank">Example on GitHub</a></figcaption>
  </figure>
  <p id="8dff">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.</p>
  <p id="db25">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.</p>
  <figure id="qcyA" class="m_retina" data-caption-align="center">
    <img src="https://miro.medium.com/v2/resize:fit:1400/1*DqJnbWO_neUGBoIA9u9oxA@2x.gif" width="480" />
    <figcaption>iOS 13 App Store</figcaption>
  </figure>
  <p id="072f">Here pagination is used with oscillations. In standard <code>UIScrollView</code>pagination, the page sizes are always the same, and pagination behavior cannot be controlled.</p>
  <h3 id="14d6">Drawer. Rubber Band Effect</h3>
  <p id="0c16">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.</p>
  <p id="5c53">But the formula for rubber band effect here needs to be used a little differently:</p>
  <figure id="0exG" class="m_retina">
    <img src="https://miro.medium.com/v2/resize:fit:1400/1*HoQnkN8y99KX_HPZBweApA@2x.gif" width="480" />
  </figure>
  <ul id="JiDK">
    <li id="4fc9">as <code>x</code>, let’s take the coordinate of the drawer, where the zero position is the upper bound of the content;</li>
    <li id="7d4d">let’s use the distance to the upper edge of the screen as the <code>dimension</code>, thus ensuring that we can’t scroll the drawer far up, off the screen.</li>
  </ul>
  <p id="8bc5">The same applies to the collapsed case:</p>
  <figure id="v2gc" class="m_retina">
    <img src="https://miro.medium.com/v2/resize:fit:1400/1*jLcu5aFyM-5SYwzNnOxSug@2x.gif" width="480" />
  </figure>
  <ul id="6TMh">
    <li id="eec4">as <code>x</code>, let’s take the position of the drawer, where the zero position is the lower bound of the content;</li>
    <li id="b624">and take the distance to the bottom of the screen as the <code>dimension</code>, thus ensuring that we can’t scroll the drawer off the screen or hide it at all.</li>
  </ul>
  <h3 id="9870">Metro Map. Scale</h3>
  <p id="fb4e">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:</p>
  <figure id="X9v3" class="m_retina" data-caption-align="center">
    <img src="https://miro.medium.com/v2/resize:fit:1400/1*XCy3n43aiiE3091Rm052Vw@2x.gif" width="480" />
    <figcaption>Rubber Band Effect for Scaling</figcaption>
  </figure>
  <h1 id="9f56">Conclusion</h1>
  <p id="25dc">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.</p>
  <p id="c555">If you want to make a smooth bound of some state, you can use rubber band effect.</p>
  <p id="aacf">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.</p>
  <figure id="gle1" class="m_retina">
    <img src="https://miro.medium.com/v2/resize:fit:1400/1*VuTokq5hgkdbwYQ9nd68Yw@2x.png" width="700" />
  </figure>
  <p id="53c7">The difference between the original formula and the approximation is that the original formula has the <em>c</em> 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 <em>x</em>.</p>
  <p id="b356">The original formula also has the <em>d</em> parameter that regulates the maximum offset and you can’t do this with an approximation at all.</p>
  <p id="1f16">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.</p>
  <p id="a75f">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.</p>
  <hr />
  <p id="c629">📚 All code examples, including PiP and <code>TimerAnimation</code>, can be found in the repository: <a href="https://github.com/super-ultra/ScrollMechanics" target="_blank">https://github.com/super-ultra/ScrollMechanics</a>.</p>
  <p id="f34a">🚀 The pull-out drawer is available as a SwiftPM package: <a href="https://github.com/super-ultra/UltraDrawerView" target="_blank">https://github.com/super-ultra/UltraDrawerView</a>.</p>
  <p id="3341">👨‍💻 I also recommend watching <a href="http://developer.apple.com/videos/play/wwdc2018/803" target="_blank">Designing Fluid Interfaces</a> and <a href="http://developer.apple.com/videos/play/wwdc2017/230" target="_blank">Advanced Animations with UIKit</a>.</p>

]]></content:encoded></item><item><guid isPermaLink="true">https://teletype.in/@swift-discipline/uiscrollview-deceleration</guid><link>https://teletype.in/@swift-discipline/uiscrollview-deceleration?utm_source=teletype&amp;utm_medium=feed_rss&amp;utm_campaign=swift-discipline</link><comments>https://teletype.in/@swift-discipline/uiscrollview-deceleration?utm_source=teletype&amp;utm_medium=feed_rss&amp;utm_campaign=swift-discipline#comments</comments><dc:creator>swift-discipline</dc:creator><title>Deceleration mechanics of UIScrollView</title><pubDate>Sun, 25 May 2025 16:19:17 GMT</pubDate><media:content medium="image" url="https://img4.teletype.in/files/f8/ae/f8aeed2c-12db-4fc1-a647-49e7716cc5c1.png"></media:content><description><![CDATA[<img src="https://img4.teletype.in/files/f1/79/f179d806-e928-4538-b5ed-fd8f63e333ab.jpeg"></img>In this article, you'll learn how UIScrollView scrolling works and how to implement it yourself.]]></description><content:encoded><![CDATA[
  <figure id="awB1" class="m_original" data-caption-align="center">
    <img src="https://img4.teletype.in/files/f1/79/f179d806-e928-4538-b5ed-fd8f63e333ab.jpeg" width="2468" />
    <figcaption><a href="https://developer.apple.com/videos/play/wwdc2018/803/" target="_blank">803 Designing Fluid Interfaces</a> / 65 / WWDC18</figcaption>
  </figure>
  <p id="9fbb">Hi! In this article, I would like to tell you how the scrolling of <code>UIScrollView</code>works, what functions are used and how to implement this mechanics by yourself.</p>
  <p id="2db4">Understanding how the scrolling works can be useful for the implementation of <code>UIScrollView</code> animation for another view with <code>UIPanGestureRecognizer</code> for example. Moreover, this mechanics was used in the implementation of the underground scheme scrolling in the multi-platform library of Yandex.Metro for iOS and Android.</p>
  <p id="6905">It is necessary to find the equation of motion to understand how the scrolling works. Once we find it, we can come up with the rest of the functions: for the scroll time, velocity and for the final position (projection) of the scrolling.</p>
  <p id="6bc4">The function of the scroll projection has already been introduced in the <a href="https://developer.apple.com/videos/play/wwdc2018/803/" target="_blank">803 Designing Fluid Interfaces</a> (WWDC18) presentation.</p>
  <figure>
    <script src="https://gist.github.com/super-ultra/34143ebd4211fa4f470a6f35d0c73877.js"></script>
  </figure>
  <p id="c3e0">But it is just the function of the scroll projection. It is not enough for either the time function or the equation of motion. But it can be used for the reference of our own calculations.</p>
  <h2 id="f550">The velocity function</h2>
  <p id="2924">Let’s come up with all the necessary functions and then write them in Swift. First of all we can find the function of velocity. The <code>.decelerationRate</code>parameter is used for <code>UIScrollView</code> configuration. It shows how much the scroll speed decelerates over time. It is important to keep in mind that the <code>UIScrollView</code> parameters are set in milliseconds, not seconds (like <code>UIGestureRecognizer</code> for example). So the <code>.decelerationRate</code> shows how much the scroll speed decelerates in one millisecond.</p>
  <p id="2b54">So if we want to use <code><a href="https://developer.apple.com/documentation/uikit/uiscrollview/decelerationrate" target="_blank">UIScrollView.DecelerationRate</a></code> then the function of velocity will look like this:</p>
  <figure id="uj1t" class="m_retina">
    <img src="https://img1.teletype.in/files/ce/bd/cebdb865-175e-4656-a16e-33e4087eb0ad.png" width="201" />
  </figure>
  <ul id="K4Rk">
    <li id="6b67">𝑣 — velocity,</li>
    <li id="e7b1">𝑣ₒ — initial velocity in pt/s (points per second),</li>
    <li id="d66a"><em>d</em> — deceleration rate (<em>0 &lt; d &lt; 1</em>),</li>
    <li id="3e21"><em>t</em> — time.</li>
  </ul>
  <h2 id="1c53">The equation of motion</h2>
  <p id="7770">Now we are ready to find the equation of motion. To do this, we should integrate the velocity function:</p>
  <section style="background-color:hsl(hsl(0,   0%,  var(--autocolor-background-lightness, 95%)), 85%, 85%);">
    <figure id="HvWD" class="m_retina">
      <img src="https://miro.medium.com/v2/resize:fit:1400/1*FQtfU4FzY6mDiNDC0uhORA@2x.png" width="369" />
    </figure>
  </section>
  <ul id="pBlm">
    <li id="6b76"><em>x </em>— position,</li>
    <li id="1a8d"><em>x</em>ₒ — initial position.</li>
    <li id="8e1f">As a result, we get the function:</li>
  </ul>
  <section style="background-color:hsl(hsl(0,   0%,  var(--autocolor-background-lightness, 95%)), 85%, 85%);">
    <figure id="jHA0" class="m_retina">
      <img src="https://miro.medium.com/v2/resize:fit:1396/1*hpGSy1PM6vJghpxC1aKMVw@2x.png" width="349" />
    </figure>
  </section>
  <ul id="lqJ1">
    <li id="9058"><em>log</em> — natural logarithm.</li>
  </ul>
  <h2 id="71a6">The final position function</h2>
  <p id="f379"><em>x(t)</em> tends to the final position as<em> t → ∞</em> because <em>0 &lt; d &lt; 1</em>:</p>
  <section style="background-color:hsl(hsl(0,   0%,  var(--autocolor-background-lightness, 95%)), 85%, 85%);">
    <figure id="hmxl" class="m_retina">
      <img src="https://miro.medium.com/v2/resize:fit:1400/1*3C4UYRAWBnPv0W6MKqMegQ@2x.png" width="411" />
    </figure>
  </section>
  <ul id="cQyf">
    <li id="2c5c"><em>X </em>— final position.</li>
  </ul>
  <p id="a111">So we got the final position function but it is not equal to the WWDC function that looks like this in our notation:</p>
  <section style="background-color:hsl(hsl(0,   0%,  var(--autocolor-background-lightness, 95%)), 85%, 85%);">
    <figure id="lDoY" class="m_retina">
      <img src="https://miro.medium.com/v2/resize:fit:1228/1*bKhz9YP67xBvGewcwQe_3A@2x.png" width="307" />
    </figure>
  </section>
  <p id="817c">Our functions differ in these parts:</p>
  <section style="background-color:hsl(hsl(0,   0%,  var(--autocolor-background-lightness, 95%)), 85%, 85%);">
    <figure id="2UnY" class="m_retina">
      <img src="https://miro.medium.com/v2/resize:fit:1156/1*A0K6BNF7xzdLOCP3zK2EbA@2x.png" width="289" />
    </figure>
  </section>
  <p id="d729">Let’s substitute the default <code>.decelerationRate</code> values from the SDK. If we substitute <code>.normal = 0.998</code> we get <code>499.5</code> and <code>499.0</code>, and if we substitute <code>.fast = 0.99</code> we get 99.5 and 99.0 respectively.</p>
  <p id="fc05">The difference is not too big. This is because the right function is a consequence of <a href="https://en.wikipedia.org/wiki/Natural_logarithm#Series" target="_blank">the decomposition of the natural logarithm in the Taylor series using the Euler transform</a>. According to the <a href="https://www.wolframalpha.com/input/?i=x+%2F+%281+-+x%29%3B+-1+%2F+log%28x%29+from+0.9+to+1" target="_blank">WolframAlpha</a>, functions behave in a similar way as <em>d</em> tends to 1.</p>
  <figure id="bODu" class="m_retina">
    <img src="https://miro.medium.com/v2/resize:fit:1400/1*LEso29qVcPM9BzGyquJbkw@2x.png" width="626.5" />
  </figure>
  <p id="159a">The function that was recommended at WWDC approximates the function we found in the neighborhood of 1, but its calculation is faster. We will continue to use the logarithm in the functions but in writing the code, we will already use a more optimal variant.</p>
  <p id="bbcc">The function that was recommended at the conference approximates the function we found in the neighborhood of 1 but its calculation is faster. In the formulas, we will continue to use the logarithm but in the code we will use the optimal variant.</p>
  <h2 id="6a72">The time function</h2>
  <p id="74e9">As a result, we found the equation of motion and the final position function. But we also need the time function to create an animation.</p>
  <p id="4bd0">When we were looking for the final position time was tending to infinity. But we cannot use infinite time. But in our case it is not necessary, the function converges to the final position. Therefore, we formulate our problem differently: we find the time when the value of the function is close enough to the final position. Let’s call the difference of values <em>ε</em> value.</p>
  <p id="8fbb">Now we need to find the time <em>T</em> which:</p>
  <section style="background-color:hsl(hsl(0,   0%,  var(--autocolor-background-lightness, 95%)), 85%, 85%);">
    <figure id="QWPR" class="m_retina">
      <img src="https://miro.medium.com/v2/resize:fit:1004/1*V6DKfNGc142PPedFs7L7hA@2x.png" width="251" />
    </figure>
  </section>
  <p id="2209">From this equation we obtain the necessary <em>T</em>:</p>
  <section style="background-color:hsl(hsl(0,   0%,  var(--autocolor-background-lightness, 95%)), 85%, 85%);">
    <figure id="ijOq" class="m_retina">
      <img src="https://miro.medium.com/v2/resize:fit:1324/1*ZWF1nVCiYhRMgvg_Oax2Eg@2x.png" width="331" />
    </figure>
  </section>
  <p id="1d0d">As the value of <em>ε</em>, you can use 0.1 (0.1 point) for example.<br />So we found all the necessary functions. Now it remains to implement Swift functions for them.</p>
  <h2 id="1966">Implementation of Swift functions</h2>
  <p id="3c19">We will assume that our points and speed are <code>CGPoint</code>. For convenience, we combine all of our original parameters in the <code>ScrollTimingParameters</code>structure, renaming <em>ε</em> to the <code>threshold</code>:</p>
  <figure>
    <script src="https://gist.github.com/super-ultra/68b6cd46c7d73b95f158efdb1b3ea11b.js"></script>
  </figure>
  <p id="6ooS">In the formulas, we used vector arithmetic operations: addition, subtraction and multiplication, and division by number. We also used the length of the vector. There are no such functions for <code>CGPoint</code> in the SDK, so we need to add them ourselves:</p>
  <figure>
    <script src="https://gist.github.com/super-ultra/57f6ffaee5be62a2d7fe75a69d9048d8.js"></script>
  </figure>
  <p id="la0o">Now we have everything we need to implement the functions. Let’s start with the final position function. We use a more accurate and less optimal formula using a logarithm as an example. If you are sure that the <code>decelerationRate</code>values in your project are always close to 1, you can replace <code>log(d)</code> with <code>(d — 1) / d</code> as in WWDC.</p>
  <figure>
    <script src="https://gist.github.com/super-ultra/bf264d7d9a4efe7757ff395e36327736.js"></script>
  </figure>
  <p id="KIFQ">Then we implement the time function:</p>
  <figure>
    <script src="https://gist.github.com/super-ultra/13ac192f11432e978d3f4f18779dec51.js"></script>
  </figure>
  <p id="FApE">Finally, the function to calculate intermediate values:</p>
  <figure>
    <script src="https://gist.github.com/super-ultra/776c81676133b4daba5dbcde4b274dd7.js"></script>
  </figure>
  <p id="EA8d">So, we have implemented all the necessary functions to implement the scrolling. As a result, we can create a scroll animation using the <code>TimerAnimation</code> which calls the given block with the current progress:</p>
  <figure>
    <script src="https://gist.github.com/super-ultra/3b6768cb4a95c6282b6b62a66a71df64.js"></script>
  </figure>
  <p id="d4a0">You can find all these functions, <code>TimerAnimation</code> implementation and the Playground example <a href="https://github.com/super-ultra/ScrollTimingParameters" target="_blank">on GitHub</a>.</p>
  <p id="c59b">However, there is not only the scrolling in <code>UIScrollView</code> but also the bounce and the rubber banding effects. We will explore how they work in one of the next articles.</p>
  <p id="cf64">—</p>
  <p id="802f">🚀 Drawer view example (like in Apple Maps or Stocks) <a href="https://github.com/super-ultra/UltraDrawerView" target="_blank">on GitHub</a>.</p>

]]></content:encoded></item></channel></rss>