●TOOLING — Rork's developer repos keep moving: rork-xcode was updated on July 16, rork-device on July 15, and rork-plist on July 13●OPUS46 — Claude Opus 4.6 is live in Rork, and Rork Max is built to assemble apps on top of Claude Code●SIM — A cloud iOS simulator runs in the browser, with one click to install on a device and two clicks to publish to the App Store●MAX — Rork Max emits pure Swift rather than React Native, reaching iPhone, iPad, Apple Watch, Apple TV, Vision Pro, and even iMessage●NATIVE — That opens up HealthKit, ARKit and LiDAR, NFC, Dynamic Island, Live Activities, 3D through Metal, and on-device inference with Core ML●SEED — Rork raised a $15M seed led by Left Lane Capital, with Peak XV and a16z Speedrun joining the round●TOOLING — Rork's developer repos keep moving: rork-xcode was updated on July 16, rork-device on July 15, and rork-plist on July 13●OPUS46 — Claude Opus 4.6 is live in Rork, and Rork Max is built to assemble apps on top of Claude Code●SIM — A cloud iOS simulator runs in the browser, with one click to install on a device and two clicks to publish to the App Store●MAX — Rork Max emits pure Swift rather than React Native, reaching iPhone, iPad, Apple Watch, Apple TV, Vision Pro, and even iMessage●NATIVE — That opens up HealthKit, ARKit and LiDAR, NFC, Dynamic Island, Live Activities, 3D through Metal, and on-device inference with Core ML●SEED — Rork raised a $15M seed led by Left Lane Capital, with Peak XV and a16z Speedrun joining the round
The Generated Screen That Quietly Jams at AX5 and in German — Putting Layout Resilience Checks on Rork Max SwiftUI
A record of running every Rork Max generated SwiftUI screen through pseudolocalization and the largest text size to find exactly where it jams. Covers when to reach for ViewThatFits, ScaledMetric and layoutPriority, plus the snapshot checks that catch regressions every time you regenerate.
Have you ever dragged the text size slider all the way to the right and then opened your own app?
I did that one afternoon with a settings screen Rork Max had built for me, and then I sat there for a while without saying anything. The toggle label ran off as "Refresh in the backgro…" and the switch that belonged beside it had been shoved clean off the edge of the display. The switch existed. My thumb simply could not reach it.
Xcode previews: green. Simulator: green. The browser live simulator, naturally, also green. At the default text size and in the default language, nothing about that screen was broken.
As an indie developer, nobody runs this kind of check on your behalf. There is no QA engineer and no review meeting. I was the one who looked at all that green and decided it was done — which leaves the job of doubting that decision with me as well.
This is a record of what happened next: putting all 23 Rork Max generated screens through pseudolocalization and the largest text size, and finding mechanically where they jam. Where they failed, how I fixed them, and how I now catch the same failures coming back after every regeneration — with the actual numbers and the actual code.
Why generated screens only ever get checked at the defaults
Rork Max emits pure Swift from a prompt. Skipping React Native means SwiftUI's layout system applies directly, which is the appeal — and it also means you inherit SwiftUI's layout weaknesses directly.
Reading through the generated code, a pattern was obvious.
Common shape in generated code
At the defaults
Under extreme conditions
Label and control dropped straight into an HStack
Fine
Label expands, control is pushed off-screen
Fixed-width button via .frame(width: 120)
Nicely sized
Text truncates to ... and loses its meaning
Absolute .font(.system(size: 15))
Exactly as intended
Ignores the text size setting entirely
Two columns assuming padding(.horizontal, 16)
Fits
Wraps one character per line in long languages
Right alignment forced with Spacer()
Cleanly flush right
Loses the tug-of-war against the label
The model is not cutting corners. If the prompt never says "must survive 310% text size," the model picks the arrangement that looks best at the defaults. That is a correct response to the instruction it was given. The problem was that I had only ever specified the defaults.
The awkward part is that this class of failure rarely reaches you through reviews. People who run their phone at large text sizes tend to delete an app they cannot operate. You do not even get the one star.
Pseudolocalization: a check you can run before translating anything
You do not need finished translations to find out whether your layout survives long strings. Xcode's pseudolanguages do it for you.
Under Scheme → Run → Options → App Language, sitting among the real languages, you will find these.
Pseudolanguage
What it does
What it surfaces
Double-Length Pseudolanguage
Repeats every string twice
Jamming, truncation and push-out in long languages
Accented Pseudolanguage
Adds accents and wraps strings in [ ]
Hardcoded, unlocalized strings
Right-to-Left Pseudolanguage
Forces a pseudo RTL presentation
Asymmetric layouts pinned to .leading
Bounded String Pseudolanguage
Draws boundaries around each string
Where clipping is actually happening
Accented is worth running first, because it catches something more fundamental than layout: strings that were never localized at all. Anything without brackets around it is baked into the source. Rork Max generally honors a String Catalog instruction, but error messages and accessibility labels did slip through in my project. I wrote that side up separately in Running Rork Max Swift Apps in Many Languages with String Catalogs.
Double-Length is the subject here. Without waiting for real German copy, you learn which parts fold first when string length roughly doubles. In practice German runs about 1.3–1.6× English, and Finnish or Hungarian stretch further still. Doubling is a little harsh, but as a margin-design load it turned out to be about right.
✦
Thank you for reading this far.
Continue Reading
What follows includes implementation code, benchmarks, and practical content we hope you'll find useful. This site runs without ads — server and development costs are supported entirely by members like you. If it's been helpful, we'd be truly grateful for your support.
WHAT YOU'LL LEARN
✦Running Xcode's Accented and Double-Length pseudolanguages against Rork Max screens: 9 of 23 jammed, and the three distinct ways they failed
✦A decision table for ViewThatFits, layoutPriority, ScaledMetric and dynamicTypeSize, with complete card code that survives AX5 (310%)
✦Snapshot test code covering 4 conditions to catch regressions after every regeneration, and how the CI run went from 11 minutes to 2m40s
Secure payment via Stripe · Cancel anytime
✦
Unlock This Article
Get full access to the rest of this article. Buy once, read anytime. This site is ad-free — your support goes directly toward keeping it running.
I narrowed things to four conditions and walked all 23 screens by hand. Testing every combination does not fit in a workable afternoon, so I kept only the combinations where breakage was plausible.
Condition
Setting
What it targets
A: baseline
English / Large (default)
The regression baseline
B: max text
English / AX5 (310%)
Vertical push-out, vanishing tap targets
C: longest copy
Double-Length / Large
Horizontal jamming and truncation
D: combined
Double-Length / AX5
Failure under the worst case
Here is what came back.
Condition
Clean
Awkward but usable
Unusable or meaning lost
A: baseline
23
0
0
B: max text
16
4
3
C: longest copy
18
3
2
D: combined
14
5
4
Condition D turned up problems on 9 of 23 screens, four of them genuinely unusable. Set against 23/23 green at the baseline, it is a fairly blunt demonstration of how little a default-settings pass actually guarantees.
The failures sorted into three types.
Type
Symptom
Count (condition D)
Root cause
Push-out
Control leaves the screen, untouchable
4
Control loses horizontal compression inside HStack
Truncation
Text becomes ... and loses meaning
3
Fixed .frame(width:)
No response
Text refuses to grow with the setting
2
Absolute .font(.system(size:))
The two "no response" cases only surfaced under condition B. Nothing is broken, so they read as green — but to someone running large text, that screen is simply the one part of the app that ignores them. Harder to notice than a break, and for that reason the one I find more troubling.
I fixed those two first, ahead of the other seven. Push-out is visible, so somebody might eventually tell you about it. Nobody tells you about no-response.
Stopping push-out: layoutPriority and ViewThatFits
Starting with the most common failure. The generated settings row looked like this.
// As generated. Correct at the defaults; the switch leaves the screen at AX5.struct SettingRow: View { let title: String @Binding var isOn: Bool var body: some View { HStack { Text(title) Spacer() Toggle("", isOn: $isOn) .labelsHidden() } .padding(.horizontal, 16) }}
When an HStack runs short on horizontal space, it compresses its children. Text resists compression, so Toggle loses first. Spacer() merely fills what is left and never enters the tug-of-war. The switch gets pushed out.
The fix came in two parts. First, make the control's priority explicit.
struct SettingRow: View { let title: String @Binding var isOn: Bool var body: some View { // Give up on the horizontal arrangement rather than break it ViewThatFits(in: .horizontal) { horizontalLayout verticalLayout } .padding(.horizontal, 16) .padding(.vertical, 8) } private var horizontalLayout: some View { HStack(spacing: 12) { Text(title) .layoutPriority(0) // the label yields Spacer(minLength: 8) Toggle("", isOn: $isOn) .labelsHidden() .layoutPriority(1) // the control holds .fixedSize() // refuse compression outright } } private var verticalLayout: some View { VStack(alignment: .leading, spacing: 8) { Text(title) .fixedSize(horizontal: false, vertical: true) Toggle("", isOn: $isOn) .labelsHidden() } .frame(maxWidth: .infinity, alignment: .leading) }}
ViewThatFits tries each candidate in order and takes the first one that fits the space it is offered. If the horizontal arrangement will not fit, it drops silently to the stacked version. It holds up better than writing the branch yourself because the decision is made on whether the thing actually fits. Pick a threshold like typeSize > .accessibility1 and that threshold will be wrong the moment the language or the device width changes.
.fixedSize() on the Toggle is what really lands. Without it, raising layoutPriority still leaves the Toggle room to shrink toward its minimum. .fixedSize() declares "I do not go below my ideal size," and that ends the tug-of-war.
Spacer(minLength: 8) is deliberate — Spacer()'s default minimum is not zero. Sometimes you want it crushed to 0 in tight spaces; here I kept 8pt so the two elements stay distinguishable.
Stopping truncation: deciding to abandon fixed widths
For the three fixed-width buttons, the fix was easy and the line-drawing was not.
Under Double-Length, "Save SettingsSave Settings" becomes Save Setti.... Nobody can tell what that button does.
Simply dropping the width gives you a button that spans the whole screen, which I wanted to avoid on design grounds. This is where it landed.
Button { save()} label: { Text("settings.save") // String Catalog key .lineLimit(2) // two lines are allowed .multilineTextAlignment(.center) .minimumScaleFactor(0.8) // shrink no further than 80% .frame(minWidth: 120) // keep only the floor .frame(maxWidth: .infinity) .padding(.vertical, 12) .padding(.horizontal, 16)}.buttonStyle(.borderedProminent).frame(maxWidth: 320) // a ceiling to stop it sprawling
Put into words, the reasoning went:
Keep only the width floor — minWidth still earns its place as a tap-target guarantee
Add a ceiling — maxWidth: 320 stops iPad stretching it across the pane
Allow two lines — insisting on one line leaves only truncation or unreadably small type
Cap shrinking at 80% — set minimumScaleFactor to something like 0.5 and unreadable text passes as "readable"
I learned the fourth one the hard way. With minimumScaleFactor(0.5), Double-Length text rendered at half size, the layout never broke, and it sailed through as green. Not breaking and being readable are different things. At 0.8, anything that will not fit properly wraps to two lines instead.
Stopping the no-response case: ScaledMetric and relative fonts
.font(.system(size: 15)) does not follow the text size setting. A text style like .font(.body) does, but generated code tends to write absolute values when the prompt mentions something concrete like "15pt."
// Does not followText(label).font(.system(size: 15))// Follows (relative, text-style based)Text(label).font(.system(.subheadline, design: .rounded))// Anchored at 15pt but still following, with a ceilingText(label).font(.system(size: 15, weight: .medium)) .dynamicTypeSize(...DynamicTypeSize.accessibility3)
What mattered more was making the non-text dimensions follow too. Leave icons and padding fixed and the text alone balloons, which breaks things in an uglier way.
struct StatCard: View { // Declare a base size and it scales with the text size setting @ScaledMetric(relativeTo: .headline) private var iconSize: CGFloat = 28 @ScaledMetric(relativeTo: .body) private var cardPadding: CGFloat = 16 @ScaledMetric(relativeTo: .body) private var cornerRadius: CGFloat = 12 let title: String let value: String var body: some View { HStack(spacing: cardPadding * 0.75) { Image(systemName: "chart.bar.fill") .font(.system(size: iconSize)) .frame(width: iconSize, height: iconSize) VStack(alignment: .leading, spacing: 4) { Text(title) .font(.subheadline) .foregroundStyle(.secondary) .fixedSize(horizontal: false, vertical: true) Text(value) .font(.headline) .fixedSize(horizontal: false, vertical: true) } Spacer(minLength: 0) } .padding(cardPadding) .background(.regularMaterial, in: RoundedRectangle(cornerRadius: cornerRadius)) }}
@ScaledMetric(relativeTo:) returns a value scaled in proportion to the given text style. At AX5 the icon grows from 28pt to roughly 62pt in practice, and because the padding grows alongside it, the proportions survive the enlargement.
.fixedSize(horizontal: false, vertical: true) on the Text refuses vertical clipping. Without it, HStack height math will sometimes flatten multi-line text into a single line. Choosing where to apply it is unglamorous work with an outsized payoff.
A summary of when to reach for what:
Tool
Where it works
Where to avoid it
ViewThatFits
Rows that should stack when squeezed
Structures with 3+ candidates (evaluation cost climbs)
layoutPriority + fixedSize
An HStack whose control must survive
When both sides are text (the tug-of-war never resolves)
@ScaledMetric
Icons, padding, corner radii
Dimensions that should not scale, like overall screen width
dynamicTypeSize(...limit)
Widgets and other places with no structural room
Body text in the main app (a ceiling becomes a gap in support)
minimumScaleFactor
Fine adjustment down to about 0.8
0.5 or below (unreadable output passes as green)
Catching what regeneration undoes, with snapshots
This next part is unavoidable once you are working with Rork Max.
Fixed layouts revert the moment you change a prompt and regenerate the screen. The model has no knowledge of the layoutPriority I added by hand. Regeneration is the whole point of Rork Max, so giving it up was never on the table. Where to draw the boundary is the subject of Regenerable Zones in Rork Max Code, but inside that boundary, "detect it when it breaks" is ultimately the only move available.
So I added snapshot checks.
import XCTestimport SwiftUI@testable import MyAppfinal class LayoutResilienceTests: XCTestCase { /// The four conditions. We deliberately do not run the full matrix. struct Condition { let name: String let locale: Locale let typeSize: DynamicTypeSize } static let conditions: [Condition] = [ .init(name: "base", locale: Locale(identifier: "en_US"), typeSize: .large), .init(name: "ax5", locale: Locale(identifier: "en_US"), typeSize: .accessibility5), .init(name: "double", locale: Locale(identifier: "en_DOUBLE"), typeSize: .large), .init(name: "worst", locale: Locale(identifier: "en_DOUBLE"), typeSize: .accessibility5), ] /// Judge overflow and clipping mechanically func assertNoOverflow<V: View>( _ view: V, named name: String, containerWidth: CGFloat = 393, // iPhone 16 logical width file: StaticString = #filePath, line: UInt = #line ) { for condition in Self.conditions { let hosted = view .environment(\.locale, condition.locale) .environment(\.dynamicTypeSize, condition.typeSize) .frame(width: containerWidth) let renderer = ImageRenderer(content: hosted) renderer.proposedSize = ProposedViewSize(width: containerWidth, height: nil) guard let image = renderer.uiImage else { XCTFail("\(name)/\(condition.name): render failed", file: file, line: line) continue } // Wider than the container means horizontal push-out let renderedWidth = image.size.width XCTAssertLessThanOrEqual( renderedWidth, containerWidth + 0.5, "\(name)/\(condition.name): width \(renderedWidth)pt exceeds container \(containerWidth)pt", file: file, line: line ) // Compare against the reference image (records on first run) assertSnapshot(image, named: "\(name)_\(condition.name)", file: file, line: line) } } func testSettingRow() { assertNoOverflow( SettingRow(title: "settings.background_refresh", isOn: .constant(true)), named: "SettingRow" ) } func testStatCard() { assertNoOverflow( StatCard(title: "stats.total_views", value: "128,400"), named: "StatCard" ) }}
Passing nil as the height in proposedSize is the key detail. Fix the height and content that ought to grow vertically gets crammed in, and the overflow stops being detectable. Fix only the width, let the height run free, and you get back the height actually required — with any width excess falling out as a plain number.
The en_DOUBLE pseudolocale is defined on the test target. Xcode's scheme setting has no effect at test runtime, so I ship a .strings file holding the doubled values.
Worth noting the CI time. My first version ran every screen × all 12 DynamicTypeSize steps × 3 locales, and took 11 minutes 12 seconds. That is not a wait you accept on every run.
The case for narrowing came out of the measured distribution.
Scope
Problems detected
Run time
All 12 steps × 3 locales
9
11m 12s
4 conditions (base/ax5/double/worst)
9
2m 40s
2 conditions (base/worst only)
7
1m 22s
Running all 12 steps found nothing the 4 conditions missed. Things that break, break at the worst case. Cutting to 2 conditions, though, dropped two cases of the "shows up at AX5 alone but is masked by a different failure in the combined case" variety. Four conditions is where I settled.
Changing the input: putting the constraints in the prompt
With detection in place, it was worth trying to have less broken code generated in the first place. I added this to the prompt.
Layout constraints:- Must not become unusable at AX5 (310%) text size- Label/control HStacks must fall back to a vertical stack via ViewThatFits- Controls must carry layoutPriority(1) and fixedSize()- Do not use absolute .font(.system(size:)); write against text styles- Declare icon and padding dimensions with @ScaledMetric(relativeTo:)- Do not use .frame(width:) on buttons; bracket with minWidth and maxWidth
The effect was, honestly, partial. Generating the same screen five times each way:
Instruction
Passes condition D, out of 5
No constraints
1 / 5
Constraints above
4 / 5
One-in-five to four-in-five is a large improvement, and it is not five in five. The more complex the screen, the more likely a couple of the constraints drop out. Which means the prompt is a tool for reducing checks, not a tool for replacing them.
There is a small disappointment in that conclusion. If instructions were honored every time, no tests would be needed. But asking a human to spot, by eye, the one run in five that missed is asking for something that will not happen. What a machine produces, a machine should verify. It feels like a reasonable place to have landed.
The order to adopt this in, and the one thing to do first
For anyone starting the same work, here is the sequence.
Run pseudolocalization once, by hand (about 30 minutes) — set the scheme's App Language to Double-Length and open every screen. You will come away with a feel for your app's weak spots
Do the same at AX5 (about 30 minutes) — Settings → Accessibility → Display & Text Size → Larger Text
List only the screens that jammed — do not try to fix everything; restrict yourself to unusable and meaning-lost cases
Apply the ViewThatFits + layoutPriority + fixedSize trio — this stops most push-out
Add snapshot checks to the fixed screens only — start with all of them and you will abandon the effort
Add the constraints to your prompt — this move only means anything once the checks exist
If you pick one, pick the first. Thirty minutes, not a line of code, and you know where your app actually stands. In my case those thirty minutes handed me nine screens' worth of homework. It was not an enjoyable half hour, but it beat hearing about it in a review.
An indie developer's hours are finite. Which is exactly why a check that surfaces nine issues in thirty minutes sits, for me, on the favourable side of the ledger.
The broader production accessibility work, VoiceOver included, is written up in Production-Quality VoiceOver and Dynamic Type for Rork Apps. That one is on the React Native side, but the thinking on label design carries over to Rork Max unchanged.
That screen at maximum text size has become the place that quietly tells me who I had been overlooking. Next time you open your app, it is worth touching that setting once.
Share
Thank You for Reading
Rork Lab is ad-free, supported entirely by members like you. We publish practical guides daily with implementation code, benchmarks, and production-ready patterns. If you've found it useful, we'd love to have you on board.