-
Notifications
You must be signed in to change notification settings - Fork 1
What SwiftUI Does
SwiftUI keeps only the ends of a value. Scritto keeps middles as well, and limits how far they travel. Everything here is measured, not read: this build's behaviour, not documented API. The reference is Text(...).contentTransition(.numericText()) on macOS 27 / Xcode 26.6, August 2026, driven through a real NSHostingView two ways: offscreen, sampling the text layer's frame every 8ms and capturing the layouts SwiftUI hands a custom TextRenderer (57 cases), and on screen, recording a 900×130 borderless window at 30fps, measuring ink travel by cross-correlating per-frame column profiles (36 cases across three sweeps). Raw matrix: research/apple-matrix.txt.
It matches a common prefix and a common suffix. Nothing else.
| shared content | example | result |
|---|---|---|
| prefix only |
abcdef → abcXY
|
prefix kept, no motion |
| suffix only |
abcdef → XYdef
|
suffix kept, slides 18px |
| both ends |
abcXXXdef → abcYYYYYdef
|
both kept, suffix slides 47px |
| middle only |
xxlightxx → yylightyy
|
not matched, everything dissolves |
| middle only, long |
zzzzdaylightzzzz → qqqqdaylightqqqq
|
not matched |
| middle only, a word |
say light now → the light is
|
not matched |
There is no search for a run flush with neither end. the light you seek is within you → you are the daylight shares the word you, and SwiftUI never considers it: the line cross-dissolves where it stands, left to right.
| pair | kept run travels |
|---|---|
dlight → light
|
25px |
daylight → light
|
69px |
brightestlight → light
|
167px |
extraordinarlight → light
|
224px |
supercalifragilisticlight → light
|
337px |
dayt → t, one glyph kept |
10px |
Every one of those is a smooth ramp, not a jump. Unlimited travel is safe for Apple because an end-anchored run never travels further than layout moves it. The distance is a consequence, not a search result.
- The text is anchored at its leading edge; the trailing edge does the moving. Across 14 recorded cases the left edge never moves.
-
.numericText()animates anything, not only digits. Words, CJK, Hebrew and emoji all transition. It is not gated on the string being a number. -
.numericText(value:)behaves identically to.numericText()in every case. -
.opacityand.identitynever interpolate the advance..interpolate, and no transition at all, snap the layout at the end. - Design (default, rounded, monospaced), size (14, 48, 96) and
monospacedDigit()change the geometry but not the rule. - A custom
TextRendereris called only for the two endpoint layouts, so the morph runs on rendered text rather than by re-laying it out per frame. - Everything not kept cross-dissolves in place, blurring slightly, staggered left to right. Glyphs never fly to a new home.
Keeps are unbounded at either end. A kept prefix or suffix travels however far layout moves it. SwiftUI slides supercalifragilisticlight → light 337px in the recording above; the same pair measured here at 48px type slides 422px. Different renderers and capture rigs, same rule.
Anything can roll. Digits, words, emoji, CJK, Arabic-Indic digits. Nothing is gated on the value parsing as a number.
The unmatched middle dissolves in place rather than flying anywhere.
Scritto searches for a run flush with neither end; SwiftUI does not. It wins real cases. 1 second → 2 seconds keeps second; Apple re-rolls the lot. xxlightxx → yylightyy keeps light standing still, measured at 2px of drift.
A floating run needs a brake, because SwiftUI's rule is self-limiting and Scritto's is not. It may travel at most its own length plus two glyph slots. That keeps light still, lets 11 → 1,001 pay 74px for the separator that pushed its digits apart, and refuses a short word crossing a long value. the light you seek is within you → you are the daylight re-rolls, same as Apple, by a different route.
Direction is read from the numbers pairwise, not from the leading one alone. Every number in the old value is compared with its counterpart in the new one and the first pair that differs decides, so 2 minutes 12 seconds → 2 minutes 13 seconds reads as a rise even though the leading number never moves. Gaining or losing a number leaves nothing to compare position-for-position, so 59 seconds → 1 minute 0 seconds reads as a rise too, which is what the clock is doing.