Why iPhone scrolling feels right

Technical notes for the video. The rubber-band formula and its constants were read straight out of Apple's iOS 27 UIKit binary. The spring numbers come from running Apple's own frameworks on a Mac.

How we got the code

Apple doesn't publish UIKit's source, but the compiled code ships inside every iOS update. iOS keeps its system frameworks in one big file, the dyld shared cache. We pulled just that file out of Apple's official iPhone 17 Pro update and disassembled UIKitCore from it with the open-source ipsw tool.

# downloads only the 6.7 GB shared cache from Apple's servers
ipsw download ipsw --device iPhone17,3 --latest --dyld --dyld-arch arm64e
# disassemble one function from UIKitCore
ipsw dyld disass dyld_shared_cache_arm64e --symbol _UIScrollViewRubberBandOffsetWithoutDecorationForOffset

Everything below is labelled by where it came from: read from the binary, measured by running Apple's code, or inferred.

1. The rubber band

When you drag past the end of a list, the content follows your finger less and less. The function that does this is _UIScrollViewRubberBandOffsetWithoutDecorationForOffset. Here is its core, from the iOS 27 binary:

0x184e596dc  fsub  d0, d0, d1     ; x = offset − edge
0x184e596e0  fdiv  d0, d0, d3     ; x / d
0x184e596e4  fmul  d0, d4, d0     ; c · x / d
0x184e596e8  fmov  d2, #1.0
0x184e596ec  fadd  d0, d0, d2     ; c·x/d + 1
0x184e596f0  fdiv  d0, d2, d0     ; 1 / (…)
0x184e596f4  fsub  d0, d2, d0     ; 1 − 1/(…)
0x184e596f8  fmul  d0, d3, d0     ; × d
0x184e596fc  fadd  d0, d1, d0     ; + edge

Read in order, those instructions are:

overscroll = (1 − 1 / (c·x/d + 1)) · d  =  c·x·d / (d + c·x)

The same function is also called by the interactive back-swipe in navigation controllers, so that edge uses the same curve.

Apple's formula (c = 0.55, d = 844)best-fit logarithmceiling = d
Finger distance past the edge vs how far the content moves, both in points. The formula levels off at one screen. A logarithm keeps climbing.

The coefficient depends on the input device

On iOS 27, -[UIScrollView _currentRubberBandCoefficient] reads an input type and looks up a built-in default (read from the binary):

InputApple's internal pref namec
Touch (default)TouchRubberBandingCoefficient0.55
Small trackpadSmallTrackpadRubberBandingCoefficient0.715
Large trackpadLargeTrackpadRubberBandingCoefficient0.17
Continuous mouseContinuousMouseRubberBandingCoefficient0.4
Hi-res wheel mouseDiscreteHighResolutionMouseRubberBandingCoefficient0.17
Low-res wheel mouseDiscreteLowResolutionMouseRubberBandingCoefficient0.5

Mac Catalyst apps use a single value instead, MacRubberBandingCoefficient = 0.075. Matching input type 0 to a finger is inferred from the pref name.

c 0.55
pull down

What it does in numbers (d = 844 pt, c = 0.55)

Finger past edgeContent movesRatio
25 pt13.5 pt54%
100 pt51.6 pt52%
844 pt (one screen)299.5 pt35%
3000 pt558.4 pt19%
∞844 pt→ 0%

Common claims, checked

The formula itself was first reverse-engineered publicly by Grant Paul in 2012 and popularised in Arkadiusz Holko's 2014 post. See also Ole Begemann. iOS 27 still uses it.

Measured check: we called UIKit's routine (Mac Catalyst build) at d = 400, 600, 844 and 1000 pt for x from 0 to 3000 pt and fit c as a free parameter. The formula fit to float precision (residual 3e-10 pt). A linear fit was off by 4–8 pt.

2. Springs instead of timers

A timed animation (an "ease") runs on a fixed path for a fixed duration. If you grab it halfway and send it back, a new timed animation starts from zero speed, so the motion stops dead for a frame. A spring is a physics simulation: it carries its current speed into the new target, so the motion stays continuous.

The model is a damped harmonic oscillator:

m·x″ = −k·(x − target) − b·x′
spring (.smooth), keeps its speed0.35 s ease-in-out, restarted
An object animates 0 → 1. At 0.12 s it's caught and sent back to 0. The spring goes 4.19 → 4.19 units/s and then turns around. The restarted ease drops from 4.08 to 0 in one instant. Computed from SwiftUI's own Spring and UnitCurve math.

Tap a toggle, then tap it again before it lands.

SwiftUI's presets are one spring with different friction

PresetResponseDamping ratioStiffness kDamping bSettles
.smooth (default)0.5 s1.0157.9125.130.80 s
.snappy0.5 s0.85157.9121.360.88 s
.bouncy0.5 s0.70157.9117.591.04 s

Mass is 1 and stiffness is (2π / 0.5)² = 157.91 in all three, so they share the same rhythm and differ only in damping. Measured by printing SwiftUI's own values on macOS 27.

Other measured defaults

Two nuances

Apple's own guidance on this is the WWDC 2018 talk Designing Fluid Interfaces.

Read, measured, inferred

Learn more

Apple's own explanations

The rubber band, explained by others

Look inside iOS yourself