iphone-duo-adaptivity-audit · git:20260913.62bff5b · 2026-09-13 · sha256 11c52f937d0a389d
iphone-duo-adaptivity-audit git:20260913.62bff5bA
Immutable. This exact content is served forever at /api/v1/blob/11c52f937d0a389d.
---
name: iphone-duo-adaptivity-audit
description: >-
Find and fix legacy screen, orientation, idiom and lifecycle code that breaks on
iPhone Duo's inner display and in other iOS 27 resizable environments (iPhone
Mirroring, iPhone apps on iPad, Split View). Use when a developer asks about
UIScreen.main or mainScreen, screen bounds or scale, interfaceOrientation or
UIDevice orientation checks, userInterfaceIdiom layout forks, keyWindow or
connectedScenes.first, migrating from app lifecycle to UIScene lifecycle
(required with the latest SDK), UIRequiresFullScreen, or portrait-only apps.
Covers SwiftUI and UIKit, ships a read-only scanner, and delegates bulk UIKit
rewrites to Xcode's own modernization skill when available. Not for app features
that merely touch UIScreen or orientation, such as screen brightness,
deliberately locking a game to one orientation, or camera and sensor setup.
---
# iPhone Duo adaptivity audit
The foundation for everything else: an app that decides layout from the screen, the
orientation or the idiom is wrong on iPhone Duo before any foldable-specific work
begins.
## Facts this skill relies on
- The **outer display** behaves like other iPhones; the **inner display** is regular
width × regular height. (Tech Talk 111461, 2:46)
- The inner display **does not honor supported interface orientations**; in iOS 27
supported orientations are a preference ignored in resizable environments, and
iPhone Mirroring always reports portrait. (111461 2:46; WWDC26 278 6:50)
- Referencing the **main screen is ambiguous on a two-display device** and deprecated
(the iOS 27 SDK header marks `mainScreen` deprecated). (111461 3:57; 278 2:51)
- The **user interface idiom is not meaningful for layout**: an iPhone app stays in the
phone idiom while fully resizable. (111461 1:33; 278 6:17)
- **UIScene lifecycle is required** when building with the latest SDKs; without it
the app no longer launches. (278 2:10)
- `UIRequiresFullScreen` is honored on iPhone in resizable environments from iOS 27
and gives discrete resizing — meant for games. (278 5:46)
## Workflow
1. **Scan** (read-only):
```bash
python3 scripts/duo_scan.py <root> --format markdown
python3 scripts/duo_scan.py <root> --format json # for exact IDs
```
Rules owned here: `DUO001` main screen, `DUO002` screen bounds, `DUO003` idiom,
`DUO004` orientation, `DUO005` app lifecycle, `DUO009` global window state,
`DUO012` scene delegate without manifest, `DUO020` `UIRequiresFullScreen`.
Member chains split across lines (`connectedScenes` … `.first`) are joined before
matching; other multi-line expressions, macros and generated code are not seen, so
follow up with targeted searches when the inventory suggests more.
2. **Classify each match** by reading the code:
- *Layout decision* (sizes, columns, which view to show) → replace.
- *Rendering scale* (`scale`, pixel alignment, image sizes) → replace with trait.
- *Physical question* no geometry answers (which end holds a cutout, sensor frame
alignment before body protocols) → **kept** if documented; otherwise ask.
- *Non-layout behavior* keyed off idiom (feature availability, analytics) → kept,
with a comment naming why.
3. **Check Xcode's skill.** Export into a temporary directory — without
`--output-dir` the command writes into the current directory:
```bash
out="$(mktemp -d)" && xcrun agent skills export --output-dir "$out" >/dev/null 2>&1; ls "$out"
```
If it yields `uikit-app-modernization` (Xcode 27.0) or App Resizability (27.1), use it for bulk
UIKit rewrites: it applies deprecate-and-forward patterns, preserves guards and
control flow, and tracks file coverage. Review its diff against the approved items.
SwiftUI code and anything it leaves as a TODO come back here.
4. **Recommend** in the format of `references/recommendation-format.md`; wait for
approval; apply; build; re-scan.
## Replacements
Full before/after code for SwiftUI and UIKit: `references/legacy-api-remediation.md`.
| Legacy | UIKit replacement | SwiftUI replacement |
| --- | --- | --- |
| `UIScreen.main.scale` | `traitCollection.displayScale` (auto-tracked in `layoutSubviews`, `draw(_:)`, `updateProperties`; else `registerForTraitChanges`) | `@Environment(\.displayScale)` |
| `UIScreen.main` (the screen) | `view.window?.windowScene?.screen`; pass a `UIScreen` parameter where no view is at hand | Rarely needed — ask what the screen is for |
| `UIScreen.main.bounds` for available space | `view.bounds` in `viewDidLayoutSubviews`; scene level: `windowScene.effectiveGeometry` + `windowScene(_:didUpdateEffectiveGeometry:)` | `GeometryReader`, `onGeometryChange(for:of:action:)`, `containerRelativeFrame` |
| `userInterfaceIdiom == .pad` for layout | `traitCollection.horizontalSizeClass` / container size | `@Environment(\.horizontalSizeClass)` |
| `interfaceOrientation`, `UIDevice.current.orientation` for layout | Size classes or `bounds.width > bounds.height` | Size classes or geometry aspect |
| Scene `interfaceOrientation` (deprecated in 27 SDK) where orientation is truly needed | `windowScene.effectiveGeometry.interfaceOrientation` | same, from the view's window scene |
| Orientation to align sensor data | `motionManager.deviceMotionBody = view`, `locationManager.headingBody = view` | same, on a hosting view |
| `UIApplication.shared.keyWindow` / `windows` / `connectedScenes.first` | `view.window`, `view.window?.windowScene` | the view's own context; `openWindow`/`dismissWindow` environment actions |
| App lifecycle only | `UIApplicationSceneManifest` + `UIWindowSceneDelegate`; move window setup to `scene(_:willConnectTo:options:)` | `@main struct …: App` is already scene-based |
## Applying changes
- **Preserve today's behavior on today's devices.** A replacement that is correct on
iPhone Duo but turns a two-column portrait / four-column landscape grid into three
columns on a small iPhone is a regression. State the before/after on at least one
existing device.
- **Kept means kept.** For a documented exception, change only the deprecated call it
uses (for example `scene.interfaceOrientation` → `effectiveGeometry.interfaceOrientation`).
Restructuring it — new representables, observers, different data flow — is a separate
proposal for the developer to approve, because the documentation exists precisely so
that nobody "improves" it unasked.
- **Typecheck what you touched.** Run the project's build, or at least
`xcrun --sdk iphonesimulator swiftc -typecheck` on the edited files at the deployment
target, and re-run `duo_scan.py`. Report what was and was not verified.
## Pitfalls
- **A size class is not a device.** Regular × regular now includes an iPhone. Code
that reads "regular means iPad" must become "regular means room for more columns".
- **Don't cache screen-derived values** in `init` or static storage; closing or
opening iPhone Duo moves the scene to another display. Recompute on trait or
geometry change.
- **Orientation replacement is not always width > height.** When left versus right
matters (cutouts, camera side), ask before replacing.
- **Scene lifecycle migration touches app state restoration, URL handling, push
notification routing and background tasks.** Plan it as its own item and read
Xcode's `scene-lifecycle-task.md` reference when available.
- Leave `UIRequiresFullScreen` alone in games that need discrete resizing; for other
apps recommend removing it and adapting instead.