跪拜 Guibai
← All articles
AI Programming

Adapting for iPhone Duo: Geometry-Driven Layout with System APIs

By 择势 ·
Read original on juejin.cn ↗ Google Translate ↗ Alt translation

Apps that hardcode device models or measured offsets break on foldables and split-screen; this design centralizes every decision behind size classes, safe areas, and reserved regions, so the same code path handles phones, the outer screen, and two-window split-screen without per-device branches. The documented pitfalls around stale caches, trait/bounds ordering, and parent-safe-area reads are directly reusable by any team adding a foldable profile.

Summary

The column algorithm starts from a fixed baseline and returns it for any compact width, so straight phones, the outer screen, and split-screen windows all keep their existing pixel layouts untouched. On regular widths, every dimension comes from system APIs: the vertical bar is already included in the safe area, `reservedRegions` reports hinge and camera occlusion, and `layoutGuide(for: .bar(onEdge:extent:))` positions bar content. The only hand-measured constants are status-bar icon offsets (34pt / 120pt) and two TabBar values (96 / 48), all flagged as tech debt pending real-device verification. Everything is wrapped behind a `DuoEnvironment` facade, pure-function `ColumnPolicy`, and three integration points: a compositional layout subclass that forces invalidation when width changes, a page base class that suspends bar-criterion responses during transitions, and a TabBar controller that writes `additionalSafeAreaInsets` for root pages.

Takeaways
— A column-count function returns the baseline for any non-regular width, guaranteeing straight phones, the outer screen, and narrow split-screen windows keep the existing layout exactly.
— The vertical bar is already in the safe area; inner landscape gives an 84pt inset, and column math uses `effectiveContentSize` so no extra deduction is needed.
— The hinge is not visible in size class; use `reservedRegions(kind: .division, options: .includeInactive)` to detect vertical vs horizontal hinge, and read it from the window root view because supplementary and early-startup views can't read it.
— Reserved regions become readable about 1s after launch and have no change notification; cache only the affirmative "has hinge" result, never the negative.
— During navigation transitions, `verticalBarEdge` is lost for the page being popped (right-bar case) because the system judges by page position; suspend bar-criterion handling while a `transitionCoordinator` exists.
— On inner/outer switch, size class changes about 60ms before bounds; a compositional layout subclass that remembers the prepared width and invalidates on mismatch fixes stale section-provider calls.
— Left split-screen leaves the window's safe area on the bar side at 0; read bar width from the parent controller's safe area minus its own `additionalSafeAreaInsets`, never from the page's own safe area.
— Trim formula is `barWidth + pageInset − 2 × pillCenter`; with 84 / 16 / 48 it gives 4pt, written as a negative `additionalSafeAreaInsets` on the bar side.
— Only two writers are allowed for `additionalSafeAreaInsets`: `AppTabBarController` for tab root pages (unified per selected tab) and `AppViewController` for other pages; the nav controller's additional is fixed at 0.
— selfCheck() pure functions replace unit tests; they run from the debug menu and list all failures instead of asserting, because a single assertion hides subsequent errors.
Conclusions

Even-column grids only matter when the hinge cuts vertically; forcing even columns on portrait makes cards 1.9× the phone width, so the policy correctly keys off the actual hinge orientation rather than a blanket rule.

Skipping feature flags for adaptation is defensible: a default-off flag would preserve a known-broken regular layout, so the team let the regular branch itself be the fix.

The status-bar icon hack (34pt / 120pt) is a reminder that public API still doesn't expose status-bar contents; apps aligning custom UI to those icons need a measured-constant escape hatch and a plan to revisit it after system upgrades.

The debugging lesson is the most transferable: during fold/rotate transitions, intermediate states like "951 wide with no bar" are self-inconsistent for tens of milliseconds, so screenshots mislead; log geometry over time before changing code.

Concepts & terms
verticalBarEdge
A trait on UITraitCollection (iOS 27.1+) reporting which screen edge a system vertical bar (status bar, Wi-Fi, camera) sits on. Returns nil when no vertical bar is present.
reservedRegions(kind:options:)
A UIView API returning regions the system reserves for hardware. `kind: .division` gives the hinge, `kind: .occlusion` gives camera cutouts. Passing `.includeInactive` includes the hinge even when not actively folded.
additionalSafeAreaInsets
A UIViewController property that appends insets to the safe area. This adaptation writes negative values on the bar side to pull content closer to the vertical bar.
Compositional layout invalidation
UICollectionViewCompositionalLayout normally invalidates only when bounds change triggers its own heuristics. The project subclasses it to force invalidation when the width differs from the last prepared width, because trait changes arrive before bounds changes.
Source: juejin.cn ↗ Google Translate ↗ Backup ↗