The app has run on iPads since day one — badly. A phone layout on an 11-inch screen just stretches: full-width list rows a metre long, charts filling the window, a capture bar like a park bench. Nothing broken, everything wrong.
"Tablet support" usually reads as redesign — breakpoints, alternate layouts, a test matrix. This one shipped as seven small phases over a few days, phones provably untouched, because of a single load-bearing decision made before any code.
TL;DR — One rule: content sits in a centered column capped at 640pt; backgrounds, headers, and bars stay full-bleed. 640 is wider than any phone (~430pt max), so phones render pixel-for-pixel unchanged — which makes every phase a tablet-only change, safe to ship one screen-group at a time. Cap the scroll content container, never the screen (or dark mode shows background seams). Fix the App Store landmine first:
supportsTablet+ portrait-only is an upload rejection (ITMS-90474) unless you opt out of Split View withrequireFullScreen: true. Cameras deliberately stay full-bleed, two-column layouts deliberately stay unbuilt — both in writing.
(Part 22 of Building CannyCart, a voice-first shopping app I'm building in public. Self-contained — no earlier context needed.)
The one decision that does all the work
From the plan's first page: single capped content column, full-bleed backgrounds. Every screen's content gets centered and capped at MAX_CONTENT_WIDTH = 640; the screen's background color, header band, and tab bar keep running edge to edge so nothing looks letterboxed.
The magic is in the number. The widest phones are ~430pt, comfortably under 640 — so on every phone, the cap never engages. Phones are pixel-for-pixel identical before and after. Which turns a scary migration into a safe sequence: every phase is tablet-only by construction, each one independently testable, each with a trivial regression check ("open it on a phone — it must look exactly like yesterday").
That's the transferable idea: pick a cap wider than any phone, and tablet support stops being a redesign and becomes a pass. A centered column is also simply how most list-driven iPad apps ship; nobody misses the two-column sidebar they never had.
Phase 1 was a config line, and it was a rejection waiting to happen
Before any layout work, the plan flagged an App Store landmine: the app had supportsTablet: true with portrait-only orientation — and Apple rejects that combination at upload with ITMS-90474 ("iPad Multitasking support requires these orientations"), because an iPad app that supports multitasking must handle every orientation and window size. The escape hatch is one line in the iOS config:
requireFullScreen: true, // portrait-only iPad app must opt out of Split View
Split View support means testing every screen at arbitrary widths — explicitly out of scope. The lesson is less the specific key than the timing: this would have surfaced on submission day, the worst possible day, if the plan hadn't hunted for store blockers first. When you add tablet support, read the store's multitasking rules before the layout code.
The mechanics: cap content, never screens
The whole implementation is one small shared file — a pageContent style merged into each screen's scroll contentContainerStyle, plus a PageContainer for non-scrolling screens. Three details carried the risk:
- The cap goes on the content container, not the screen. The ScrollView still spans the full width (so the scrollbar hugs the screen edge and the background paints everywhere); only its content column is capped and centered. Cap the screen instead and dark mode shows background seams at the column edges — the plan's test checklist calls this out per phase, light and dark.
- Bottom sheets got capped once, centrally. Every sheet in the app already rendered through one shared sheet component, so tablets got centered, width-capped sheets in a single change. The recurring dividend of routing UI through shared components: an N-screen problem was a one-component problem.
-
Charts had to stop sizing from the raw window. Part 16's charts size themselves from
useWindowDimensions— correct on phones, comically wide on an iPad. They now size fromuseContentWidth()(the window width, capped) — same hook family, one honest indirection. Anything pixel-sized needs the capped width; anything flexible just flows into the column.
The keyboard-anchored capture bar was the one medium-risk phase: a bar that floats above the keyboard must visually match the capped column above it, or the misalignment shouts. Same fix — cap the bar's content, keep its background full-bleed.
What deliberately stayed full-bleed, and what stayed unbuilt
- Camera screens and the receipt photo viewer are untouched. A live viewfinder and a pinch-zoomable photo should fill any screen on any device; capping them would be design-system compliance beating usability.
- Two-column layouts (Home cards 2-up, master-detail) are written down and not built — an optional phase-8 doc for later. The column ships value now; the fancy layout is a decision deferred in writing, which is the difference between scope discipline and scope amnesia.
The later twist: landscape, per form factor
The plan said portrait-only everywhere. A later pass loosened that — but per form factor: the iPad now rotates freely (iPad-specific orientation entries), while iPhones stay portrait-locked, and Android tablets stay portrait until a runtime orientation unlock ships. Two things made that safe:
- The centered column is naturally rotation-friendly — in landscape it's the same 640pt column with more margin. No layout work; the pass had already paid for rotation.
-
requireFullScreen: truestays, even with all orientations supported. Drop it and you're back to owing full Split View support — arbitrary window widths — which is a different, much larger contract than "rotates on iPad."
What I took away
- Find the number that makes the migration free. A cap wider than any phone meant zero phone risk, which meant shippable phases instead of a big-bang redesign.
- Store blockers are config, so fix them in phase 1 — ITMS-90474 on submission day would have cost a release cycle; in the plan it cost one line.
- Cap the content container, never the screen — and check both themes, because seams hide in light mode.
- Shared components turn passes into patches. One sheet component, one chart-width hook, one page container — most phases were an import and a merge.
- Write down the non-goals. Full-bleed cameras, unbuilt two-column, Split View opt-out: each a sentence in the plan, each a scope argument nobody has to re-have.
Next up
Part 23 leaves layout for plumbing: push notifications end-to-end — token hygiene on shared devices, a timezone-aware weekly reminder, and the admin broadcast composer that sends them.
What's your equivalent of the 640pt cap — the single constraint that turned a scary migration into a boring sequence of small diffs?
Top comments (0)