Make text scale with Dynamic Type - #557
Merged
Merged
Conversation
bradleymackey
force-pushed
the
native-standardization-forms
branch
from
September 14, 2026 16:50
9c52731 to
4c73962
Compare
The app's primary content — the OTP code — was a fixed 36pt, and its status text was 7pt. Neither responded to Dynamic Type at all. ## Fixed point sizes become text styles | Location | Before | After | | --- | --- | --- | | `LoadingBarLabel` | `.system(size: 7, weight: .semibold)` | `.caption2.weight(.semibold)` | | `EncryptedItemPreviewView` badge | `.system(size: 9, weight: .medium)` | `.caption2.weight(.medium)` | | `TOTPCodePreviewView`, `HOTPCodePreviewView`, `OTPWidgetSmallView` | `.system(size: 36, design: .monospaced)` | `.system(.largeTitle, design: .monospaced)` | | `OTPCodeButtonView` | `.system(size: 24, weight:)` | `.title2.weight(_:)` | | `BackupImportCodeStateVisualizerView` | `.system(size: 28)` / `.system(size: 24).bold()` | `.largeTitle` / `.title2.bold()` | 7pt is well below the legibility floor, and it was the only text carrying "Code locked", "Update required" and the error titles. The OTP code keeps its exact monospaced look — `.system(_:design:)` has a *text style* overload, which `OTPCodeTextView` was already using in its own preview. Only the scaling behaviour changes. The three remaining `.system(size:)` uses are decorative glyphs (the 64pt autofill header, the 100pt QR placeholder, and `FormRow`'s tile glyph) and are left alone. ## The `String.count` font ladders are gone Five copies of the same anti-pattern — a `switch` on the title's character count picking between a text style and two or three fixed point sizes: - `TOTPCodePreviewView`, `HOTPCodePreviewView` and `OTPWidgetSmallView` held **the same `issuerFont` three times** - `SecureNotePreviewView` had an eight-way tuple variant - `EncryptedItemPreviewView` a four-way one Every one bottomed out at a fixed 14–20pt, so a user at an accessibility text size saw 14pt text inside a card sized for their setting — the exact inversion of what Dynamic Type is for. Each is replaced by one text style plus `.minimumScaleFactor(0.7)` and `.allowsTightening(true)`, which is what actually does the fitting. `SecureNotePreviewView` keeps its one meaningful distinction — the title is heavier when there is no description to share the card with — now expressed as a single ternary rather than a tuple ladder. ## Metrics that box text now scale - `FormRow`'s 28pt glyph tile and `PlaceholderView`'s 40pt icon frame become `@ScaledMetric`. The `PlaceholderView` frame was clipping a `.largeTitle` glyph at accessibility sizes. - `OTPCodeDetailView`'s preview card: `.frame(width: 180)` → `.frame(maxWidth: 240)`. - `VaultAboutView`'s logo: `.frame(height: 21.6)` → `22`. A fractional point height lands off the pixel grid. ## The global appearance proxy is gone `VaultMainScene.init` called `UITextView.appearance().textContainerInset = …`, which applied to every `UITextView` in the process — including ones the app does not own. The three `TextEditor`s that relied on it now set `.contentMargins(12, for: .scrollContent)` themselves. `SelectableText` also relied on it and is not a SwiftUI scroll view, so it sets `textContainerInset` on its own `UITextView`. While there, `SelectableText` stops scaling a hardcoded 16pt base and derives from the text style's own font, so weight and tracking match the style. Note `preferredFont(forTextStyle:compatibleWith:)` is already scaled for the given content size category — it must not also be passed through `UIFontMetrics`, which would scale it twice. ## Verification Local, iPhone 18 Pro Max / iOS 27.0: - `xcodebuild build-for-testing` — `TEST BUILD SUCCEEDED` - Full suite, `-parallel-testing-enabled NO` — `TEST EXECUTE SUCCEEDED`, 2834 passed, 0 failures across 24 bundles - 150 snapshots re-recorded, of which 101 changed, across 12 suites; the remaining 49 re-recorded byte-identical. Reviewed with attention to the `xxLarge` variants, which are the point of the change. - `make format` + `make lint` — clean⚠️ Automatic CI is still disabled (#548), so this is local verification only. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
bradleymackey
force-pushed
the
native-standardization-dynamic-type
branch
from
September 14, 2026 16:50
ac0af5d to
2b07fb4
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The app's primary content — the OTP code — was a fixed 36pt, and its
status text was 7pt. Neither responded to Dynamic Type at all.
Fixed point sizes become text styles
LoadingBarLabel.system(size: 7, weight: .semibold).caption2.weight(.semibold)EncryptedItemPreviewViewbadge.system(size: 9, weight: .medium).caption2.weight(.medium)TOTPCodePreviewView,HOTPCodePreviewView,OTPWidgetSmallView.system(size: 36, design: .monospaced).system(.largeTitle, design: .monospaced)OTPCodeButtonView.system(size: 24, weight:).title2.weight(_:)BackupImportCodeStateVisualizerView.system(size: 28)/.system(size: 24).bold().largeTitle/.title2.bold()7pt is well below the legibility floor, and it was the only text carrying
"Code locked", "Update required" and the error titles.
The OTP code keeps its exact monospaced look —
.system(_:design:)has atext style overload, which
OTPCodeTextViewwas already using in itsown preview. Only the scaling behaviour changes.
The three remaining
.system(size:)uses are decorative glyphs (the 64ptautofill header, the 100pt QR placeholder, and
FormRow's tile glyph)and are left alone.
The
String.countfont ladders are goneFive copies of the same anti-pattern — a
switchon the title's charactercount picking between a text style and two or three fixed point sizes:
TOTPCodePreviewView,HOTPCodePreviewViewandOTPWidgetSmallViewheld the same
issuerFontthree timesSecureNotePreviewViewhad an eight-way tuple variantEncryptedItemPreviewViewa four-way oneEvery one bottomed out at a fixed 14–20pt, so a user at an accessibility
text size saw 14pt text inside a card sized for their setting — the exact
inversion of what Dynamic Type is for.
Each is replaced by one text style plus
.minimumScaleFactor(0.7)and.allowsTightening(true), which is what actually does the fitting.SecureNotePreviewViewkeeps its one meaningful distinction — the titleis heavier when there is no description to share the card with — now
expressed as a single ternary rather than a tuple ladder.
Metrics that box text now scale
FormRow's 28pt glyph tile andPlaceholderView's 40pt icon framebecome
@ScaledMetric. ThePlaceholderViewframe was clipping a.largeTitleglyph at accessibility sizes.OTPCodeDetailView's preview card:.frame(width: 180)→.frame(maxWidth: 240).VaultAboutView's logo:.frame(height: 21.6)→22. A fractionalpoint height lands off the pixel grid.
The global appearance proxy is gone
VaultMainScene.initcalledUITextView.appearance().textContainerInset = …, which applied to everyUITextViewin the process — including ones the app does not own.The three
TextEditors that relied on it now set.contentMargins(12, for: .scrollContent)themselves.SelectableTextalso relied on it and is not a SwiftUI scroll view, so it sets
textContainerInseton its ownUITextView.While there,
SelectableTextstops scaling a hardcoded 16pt base andderives from the text style's own font, so weight and tracking match the
style. Note
preferredFont(forTextStyle:compatibleWith:)is alreadyscaled for the given content size category — it must not also be passed
through
UIFontMetrics, which would scale it twice.Verification
Local, iPhone 18 Pro Max / iOS 27.0:
xcodebuild build-for-testing—TEST BUILD SUCCEEDED-parallel-testing-enabled NO—TEST EXECUTE SUCCEEDED,2834 passed, 0 failures across 24 bundles
remaining 49 re-recorded byte-identical. Reviewed with attention to the
xxLargevariants, which are the point of the change.make format+make lint— clean🤖 Generated with Claude Code