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:
- x: how far past the edge your finger has dragged
- d: the scroll view's size along that axis (roughly one screen)
- c: a coefficient, 0.55 for touch (below)
The same function is also called by the interactive back-swipe in navigation controllers, so that edge uses the same curve.
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):
| Input | Apple's internal pref name | c |
|---|---|---|
| Touch (default) | TouchRubberBandingCoefficient | 0.55 |
| Small trackpad | SmallTrackpadRubberBandingCoefficient | 0.715 |
| Large trackpad | LargeTrackpadRubberBandingCoefficient | 0.17 |
| Continuous mouse | ContinuousMouseRubberBandingCoefficient | 0.4 |
| Hi-res wheel mouse | DiscreteHighResolutionMouseRubberBandingCoefficient | 0.17 |
| Low-res wheel mouse | DiscreteLowResolutionMouseRubberBandingCoefficient | 0.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.
What it does in numbers (d = 844 pt, c = 0.55)
| Finger past edge | Content moves | Ratio |
|---|---|---|
| 25 pt | 13.5 pt | 54% |
| 100 pt | 51.6 pt | 52% |
| 844 pt (one screen) | 299.5 pt | 35% |
| 3000 pt | 558.4 pt | 19% |
| ∞ | 844 pt | → 0% |
Common claims, checked
- wrong"It's logarithmic." It's a rational function (a hyperbola) with a hard ceiling at d. A log never levels off. At 10 screens of drag, the best-fit log gives 825 pt where Apple's formula gives 714 pt.
- wrong"It cuts your movement by 55%." Near the edge the content moves 55% of your finger, which is a 45% cut. And the cut isn't fixed: the slope is c / (1 + c·x/d)², so after one screen of drag each extra point of finger moves the content only 0.23 pt.
- right"c = 0.55." Confirmed in the binary, but only for touch. Trackpads and mice get other values.
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:
Tap a toggle, then tap it again before it lands.
SwiftUI's presets are one spring with different friction
| Preset | Response | Damping ratio | Stiffness k | Damping b | Settles |
|---|---|---|---|---|---|
| .smooth (default) | 0.5 s | 1.0 | 157.91 | 25.13 | 0.80 s |
| .snappy | 0.5 s | 0.85 | 157.91 | 21.36 | 0.88 s |
| .bouncy | 0.5 s | 0.70 | 157.91 | 17.59 | 1.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
.interactiveSpring(): response 0.15, damping 0.85, blend 0.25.easeInOut: a 0.35 s Bézier curve- UIKit
UISpringTimingParameters()with no arguments: mass 3, stiffness 1000, damping 500. That's a damping ratio of 4.56, heavily overdamped, and settles in 0.6 s. - Core Animation
CASpringAnimation(): m = 1, k = 100, c = 10, settles in 1.47 s
Two nuances
- Carrying speed through an interruption is something the code chooses to do by passing the current velocity in (
UISpringTimingParameters(dampingRatio:initialVelocity:)). In Core Animation, the same spring started with zero velocity also stops dead. - SwiftUI blends an interrupted
.easeInOutwith the old animation, so in SwiftUI an ease doesn't jump either. Springs retarget through physics, while eases get layered on top of each other.
Apple's own guidance on this is the WWDC 2018 talk Designing Fluid Interfaces.
Read, measured, inferred
- Read from the iOS 27 binary: the rubber-band formula and the per-device coefficient table
- Measured by running Apple's code on a Mac: the rubber-band curves (Catalyst UIKit), all spring and animation parameters, and the interruption traces
- Inferred: that input type 0 is a finger, and that iOS's pan handler feeds the raw finger offset into this function
- Not done: nothing was measured on a physical iPhone. Apple can also override these values through internal preferences.
Learn more
Apple's own explanations
- Designing Fluid Interfaces (WWDC18). Apple's designers on why interfaces should follow your finger and carry momentum. The best place to start.
- Animate with springs (WWDC23). Where SwiftUI's response and bounce parameters come from.
- SwiftUI Spring and UISpringTimingParameters. Developer docs for the springs measured above.
The rubber band, explained by others
- UIScrollView's Inertia, Bouncing and Rubber-Banding by Arkadiusz Holko. Rebuilds the scroll view, formula included.
- Inertia, Bouncing, and Rubber-Banding by Ole Begemann. The same problem approached with a physics engine.